Compare commits
	
		
			1432 Commits
		
	
	
		
			v0.0.1
			...
			c1a80e50e7
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c1a80e50e7 | |||
| 52962f3a5e | |||
| b3f095b61f | |||
| a5004c8ba9 | |||
| 7d9b1b508b | |||
| 5e265dfc83 | |||
| 3a43d6f8ac | |||
| 11a6649847 | |||
| 7caf4a0173 | |||
| 385524352c | |||
| 5ca5323782 | |||
| ba6da856bb | |||
| c0e72246cc | |||
| c7ab5447ea | |||
| 5fdd461159 | |||
| 421955f2a0 | |||
| a28f6985ed | |||
| 8244dddab7 | |||
| a5ca436eaa | |||
| d7fc1c2c88 | |||
| 382627ef8d | |||
| 17667b4cf8 | |||
| 5231ec22e7 | |||
| 929ae1b709 | |||
| f01f7a5ab9 | |||
| a2dce833f8 | |||
| de6c7a4fd4 | |||
| 4edee0f7f6 | |||
| 988a807fa4 | |||
| 5258e4253d | |||
| 09ba86dec5 | |||
| 78d8a1aa23 | |||
| 22def15209 | |||
| 4cbda7a849 | |||
| be85a620ef | |||
| 0b07b678b4 | |||
| 4733ce9287 | |||
| 48d6bf4c15 | |||
| 8c759bcbac | |||
| b5ed7014f6 | |||
| 6cd9dea186 | |||
| 202b416acf | |||
| 93d46f5610 | |||
| c5ddf3ac99 | |||
| a9cb913a47 | |||
| b7b5d4f1a5 | |||
| a947396bad | |||
| d528bc808e | |||
| c6fd05c2cf | |||
| d6bb9d311a | |||
| 53b4cbbf8c | |||
| 628716ec28 | |||
| bd14168627 | |||
| 96037d4da6 | |||
| 5448e773d8 | |||
| 848ef21c7c | |||
| 2ecae7da93 | |||
| d9ce569eb9 | |||
| eacaf392b1 | |||
| ce16592b6a | |||
| 295d76d354 | |||
| 23b3c998bd | |||
| b5e966c9a1 | |||
| 96cb6f4b12 | |||
| e2c0f82ec0 | |||
| dbf28c03e6 | |||
| 26165e30de | |||
| c52331a23a | |||
| 8007e71e1d | |||
| 28d08e013f | |||
| 64bbd383de | |||
| 8a9f53102b | |||
| 0412b97170 | |||
| c8b8a8fc03 | |||
| 95d3090b9b | |||
| 49129ee6dd | |||
| 6a7ecb0d4a | |||
| 1ceeed1007 | |||
| a7922ff44e | |||
| a421604ed5 | |||
| 7d182db32f | |||
| c5cb9979d3 | |||
| b9a73106ed | |||
| c674cca482 | |||
| 81d1228b92 | |||
| 6ae61d5b81 | |||
| 9cb872eec2 | |||
| 68e8c010b7 | |||
| 9671413906 | |||
| 4c8d24c319 | |||
| e50144bd34 | |||
| 9f3171e3f1 | |||
| cc92748747 | |||
| 0a0b0c1adb | |||
| 92a74026a6 | |||
| 3fa1c6c420 | |||
| b04eccdbda | |||
| 9ce30dee70 | |||
| 3c0b680b8e | |||
| 895356897b | |||
| 9164be2f37 | |||
| 5385264f94 | |||
| 610e756c07 | |||
| 15c9f8f458 | |||
| fb704a5b83 | |||
| fdda628be8 | |||
| 2b45d8aa05 | |||
| 0e2fc65301 | |||
| e8ef7e74de | |||
| c32e1b9583 | |||
| 9d0f6ec155 | |||
| 855d603795 | |||
| af25782185 | |||
| e5ba51b80a | |||
| 5e240de677 | |||
| 418cfac0e3 | |||
| 9d09607013 | |||
| eddf25b622 | |||
| 537a8654fa | |||
| 9de33d06d2 | |||
| 0e5f320664 | |||
| 88d8e60511 | |||
| 439f07162e | |||
| efe2b6cbd9 | |||
| 0aa1ed9464 | |||
| cb94ed6a2a | |||
| cf187ee46b | |||
| 3e71fc20fd | |||
| f3601321f7 | |||
| 540059368c | |||
| 7ce89123f7 | |||
| e3c7c86212 | |||
| 794804e27f | |||
| 6d89c1da6e | |||
| d059554464 | |||
| 3a392d4a9f | |||
| e3071b372a | |||
| 18bd279b0c | |||
| 5b93db7463 | |||
| 5b7e5eb91b | |||
| 78ca383e3c | |||
| c1eed9ada3 | |||
| 8d6feb5394 | |||
| 42994f8977 | |||
| f0a871e1f8 | |||
| a710c30572 | |||
| c991763b00 | |||
| 72dae14f87 | |||
| 5800340762 | |||
| c5f5adcac6 | |||
| 591642efb3 | |||
| 6182ffa1d4 | |||
| 402a898d96 | |||
| 13d43d8319 | |||
| 7bcdbd3813 | |||
| 60ada22674 | |||
| 637119d46d | |||
| 40f3da6a65 | |||
| f4697fe7f7 | |||
| 3bc18b9021 | |||
| c21581aefa | |||
| 165f25db69 | |||
| 9aa0617aa1 | |||
| ddce88dce6 | |||
| 6aa2bce2be | |||
| a43c1d3d1e | |||
| 1ed0e817e8 | |||
| 709ca55e65 | |||
| 8c13f5dbba | |||
| 4cb82d81b7 | |||
| 0c42921387 | |||
| 70a3e7fc7d | |||
| d5267be38c | |||
| 8e7e0ed490 | |||
| 8cf2837725 | |||
| 63ae186c76 | |||
| dbf5c7b832 | |||
| bfbfc01e99 | |||
| 8fa9d0e843 | |||
| 2d3e108fd9 | |||
| 7822b30dcb | |||
| 2701b7d04e | |||
| e361c3f975 | |||
| 260706c172 | |||
| 390668ec34 | |||
| 1d5cdf9607 | |||
| a4bf3542e0 | |||
| df82cfe66b | |||
| f23414adaf | |||
| 41024ddb79 | |||
| 53f9547cc5 | |||
| 4bfd9de100 | |||
| c01e00d77d | |||
| 825191c08f | |||
| 9dc6670795 | |||
| 1db8eee9f7 | |||
| 1bc50cb62c | |||
| 450b07fd08 | |||
| 12c7515ee8 | |||
| ed65da4340 | |||
| d9d2917cf5 | |||
| ce5ca1875b | |||
| 4f869252a2 | |||
| 17b92126de | |||
| 6e88c44229 | |||
| 6c3d338c12 | |||
| 4ebd44cb4e | |||
| 75cb9f7fd2 | |||
| eacca9d2ab | |||
| d0e11bc68b | |||
| 1958623a7a | |||
| 498d8b6520 | |||
| a12f2fec5a | |||
| 22bf046643 | |||
| dca48fae36 | |||
| 9af4068bb6 | |||
| 2992d8ec12 | |||
| 33dd2560e0 | |||
| aeb5c6ee25 | |||
| 08a2436b8f | |||
| fbc3cfeda4 | |||
| c8812b1add | |||
| 8d82e80639 | |||
| ed741d53d7 | |||
| 685754895b | |||
| e7791d38ff | |||
| 9f14653001 | |||
| 6c5a7b0751 | |||
| 51a327c52d | |||
| 5a978bb30d | |||
| 6801758cb3 | |||
| 14de3dd9e5 | |||
| ed2d57fb4b | |||
| e87acc6286 | |||
| 0de932bc9e | |||
| d021d9f757 | |||
| eb5da26004 | |||
| 6765254f43 | |||
| e98802f5b2 | |||
| af54b6483e | |||
| 96167c3167 | |||
| eecfdf482f | |||
| 7ceb865206 | |||
| b919670706 | |||
| 72120b8842 | |||
| 1e53c08d9d | |||
| 2d1b6a09e9 | |||
| 7f661d9af9 | |||
| 81c66bdddd | |||
| 4bd46a1657 | |||
| 244a752ae1 | |||
| 72369ab745 | |||
| b62a05f627 | |||
| 03eb8e7fae | |||
| a5a00b6987 | |||
| 542162c78a | |||
| 8cfe0fb7d2 | |||
| 49c831cb62 | |||
| 2c79e03094 | |||
| 21e6cf10b6 | |||
| dc655bb359 | |||
| b9987580ee | |||
| cb2dfc696d | |||
| 7f0643f9c0 | |||
| 14a4117aff | |||
| 55fb5dce1a | |||
| 923d6f9835 | |||
| 08b5ade8ec | |||
| 91f41c7497 | |||
| fa06282ff9 | |||
| 48b967f5b6 | |||
| f479165aac | |||
| 2f83ecc1ac | |||
| 01efc215fd | |||
| 00ba74a6c4 | |||
| 44b5ba1a9a | |||
| 843e172e56 | |||
| a0df336abe | |||
| 0db4bb06c9 | |||
| ad2b49b838 | |||
| ab519342e8 | |||
| 1f0b9012e3 | |||
| 1ddad6be93 | |||
| cf311003c0 | |||
| 64249976a8 | |||
| 6ecb3ccd08 | |||
| 4867bacb72 | |||
| 7d029d3d7a | |||
| 455befc18f | |||
| 6e57845512 | |||
| 1335a6e1e5 | |||
| 1eab44464c | |||
| c3e2da3d51 | |||
| 1716f71c12 | |||
| b52e99c958 | |||
| 86531bfd7e | |||
| 874a22325e | |||
| 2380b65853 | |||
| f72e8cbd91 | |||
| 24e418344e | |||
| 2b7077ca70 | |||
| 10d438e723 | |||
| 331846ee2e | |||
| dc0e58afc1 | |||
| 18e9252998 | |||
| b2e3c04036 | |||
| 4fd155e68a | |||
| 59ac0b5f20 | |||
| f4979c841a | |||
| 74eb74deb1 | |||
| 9e5e7b70d4 | |||
| 2384fc9fa9 | |||
| 576e58b1e3 | |||
| a0af058f5e | |||
| b40457d774 | |||
| 2353b43514 | |||
| b11d5192c2 | |||
| d38c58ce1d | |||
| a0f390b7dc | |||
| cb12799111 | |||
| 86fb5c53a1 | |||
| 29fc728509 | |||
| 0fb341f378 | |||
| 8a1a182479 | |||
| 49907bc8ee | |||
| 21d4a9b328 | |||
| d5ede43a13 | |||
| b73f5011cf | |||
| 32ebfa78cd | |||
| 39c942a205 | |||
| ebc4533b10 | |||
| 4e5f9c86a8 | |||
| d89a7a5556 | |||
| 8ab53f2da3 | |||
| 4c8eab2692 | |||
| 08989f54d9 | |||
| c78753f3ff | |||
| 34a87d8b3b | |||
| 7516524d69 | |||
| 549d7ffec8 | |||
| ccafc23d3c | |||
| 709b57d84f | |||
| 9ef909c9a1 | |||
| d7c0ffaac4 | |||
| e4cd5312f1 | |||
| 197fca6d3b | |||
| 04af1f0053 | |||
| 93d9b1ed93 | |||
| 2d73116bc0 | |||
| f2f6d78790 | |||
| 797509fc11 | |||
| 6920504762 | |||
| 9d1476a760 | |||
| c1890775dc | |||
| 72e5fe5b8f | |||
| c81ec214e2 | |||
| 0dcc879eb1 | |||
| 4f3f4295ea | |||
| d02f17a8cf | |||
| 2f6a92168e | |||
| b6a3923b27 | |||
| d556cbc835 | |||
| f186806117 | |||
| f4f560b164 | |||
| 14b7f9237b | |||
| f3518b3d0f | |||
| 7964524e0a | |||
| 8ab8335baa | |||
| cd43bf9dfa | |||
| ccebf831e7 | |||
| 9f2f9bd8b0 | |||
| adf8c14536 | |||
| 606e82d718 | |||
| 1621f1753a | |||
| 196ab66e14 | |||
| 38ab32dad9 | |||
| 86046e52f0 | |||
| 9e7c860414 | |||
| 7dc8b86ee2 | |||
| 6ecbfe3de6 | |||
| f9940fc436 | |||
| 58e75ee276 | |||
| e7771f539d | |||
| c2f62cd8e0 | |||
| f4b6812675 | |||
| 03e4b37c04 | |||
| 7b3a9e0f63 | |||
| 067f546580 | |||
| 2f7697b7ec | |||
| 1d214f89ed | |||
| 0b47207949 | |||
| 94dd573a81 | |||
| 6fa4896155 | |||
| 28c99f9d8b | |||
| 88fbb5f73b | |||
| 402c185dd4 | |||
| ae2015a604 | |||
| 023731fc3f | |||
| 99998aac8a | |||
| 360d0bc110 | |||
| 817838e522 | |||
| deb3cfb4b6 | |||
| af61519632 | |||
| b1714cf554 | |||
| f0984b19f2 | |||
| eb3c9cd6f3 | |||
| e677b0ac3c | |||
| dd909bfe53 | |||
| b13b111614 | |||
| 5511530926 | |||
| 5e1ef01bc0 | |||
| a060eadab7 | |||
| 70db31bb8f | |||
| 1292775a75 | |||
| 0fbc84d364 | |||
| 0dd0b835ec | |||
| d96e0a7497 | |||
| 625504b8eb | |||
| a185ded47e | |||
| 5a0c77d06a | |||
| e54bd316d5 | |||
| f908b45cc7 | |||
| d02751ee08 | |||
| 13ab9786f7 | |||
| ba13a08e78 | |||
| 8c92a5ff7b | |||
| 37f728835b | |||
| a6f1eaa09e | |||
| dff8eca16e | |||
| a3cca9aae6 | |||
| edabd7735c | |||
| 461e7b7d5a | |||
| 06ea8d4781 | |||
| 62ef0bcb42 | |||
| a0043ec49f | |||
| 305f5232e7 | |||
| d467c4dd8a | |||
| 5b2ace80d4 | |||
| 1484a87cad | |||
| b23bc0b16b | |||
| a6c8dd846c | |||
| 5fff3b8161 | |||
| b4236d0ec0 | |||
| 5e8cd12760 | |||
| 699438602c | |||
| 52aa6eed0d | |||
| 6cdf207dcd | |||
| 607e9ef71b | |||
| 367c489fc3 | |||
| b3c9ad2fcb | |||
| ee9cb63327 | |||
| 889773c38d | |||
| b83704a218 | |||
| dfe5d51d04 | |||
| 280dee0438 | |||
| aa10ab69f6 | |||
| 6ef14d985d | |||
| 61a3226e14 | |||
| f9c370212b | |||
| 021f3ad5bc | |||
| 8bdc27bf5c | |||
| 00eb5222f8 | |||
| d06f490cc2 | |||
| b087a09d37 | |||
| 4cedc54d2d | |||
| 8fe7adc50e | |||
| bf5236e68b | |||
| da1d686705 | |||
| 2ce5bc73d5 | |||
| c6ae9313cc | |||
| 8f3883563f | |||
| 950273da41 | |||
| 391da742fd | |||
| 4414676076 | |||
| 0da45b7b40 | |||
| 7e02cb90f6 | |||
| 01ba90fdba | |||
| b394140f9e | |||
| 4a1d136721 | |||
| ab3009f771 | |||
| bf340f3de4 | |||
| 68f5827dee | |||
| b695a4ba3b | |||
| d84ab2734e | |||
| 4f0cc793c7 | |||
| b7a4ac22b2 | |||
| 5db9acae1d | |||
| c299c1432c | |||
| d8551ab732 | |||
| 25bc1279c2 | |||
| f6d9d23456 | |||
| b20d95d616 | |||
| 071c2f1c20 | |||
| 566d00f0df | |||
| 0550aa4e98 | |||
| baf69355a5 | |||
| 17c0266998 | |||
| 14a2207064 | |||
| 7c721fc6eb | |||
| ab03692a4c | |||
| 626fa4f27b | |||
| 15676e0f4f | |||
| 8709ce8ad5 | |||
| bb924d79d6 | |||
| 8e075e33d9 | |||
| b0b002104a | |||
| 43d1f34fa3 | |||
| 6db1a097aa | |||
| 6dae2f0749 | |||
| dc87c26b99 | |||
| 234d597083 | |||
| b74c347c7c | |||
| 996996e609 | |||
| cd9050f61f | |||
| b70b309977 | |||
| ee510f3f3f | |||
| 8a70b8ea3e | |||
| 27ee73bb89 | |||
| 3aeb47e447 | |||
| b9ceb30ecf | |||
| 5b6ee20b2d | |||
| d062ec0dfe | |||
| d5a0daa0d3 | |||
| 8c4ec71e26 | |||
| 1e5aa0ba93 | |||
| 0326a1f8cc | |||
| 8311404a09 | |||
| c81111c2cf | |||
| e499be12ae | |||
| e785f7f10a | |||
| c3da10bef6 | |||
| c8c8cb305e | |||
| efdd046ef8 | |||
| 496eefd2ee | |||
| 9da79b3a21 | |||
| 1b2b0970fb | |||
| 0bb45a7fa8 | |||
| 15a25a41aa | |||
| 76b6ff78cd | |||
| 5285b3f222 | |||
| 0c993c251b | |||
| b3a1f17452 | |||
| 11929e8c68 | |||
| fc9a081250 | |||
| 2583221117 | |||
| a69e551968 | |||
| 8bd0027e71 | |||
| 84eaa3e2fd | |||
| a57916b3db | |||
| 87e769786a | |||
| 05a7e941cf | |||
| ed307e6b3b | |||
| e8aa957209 | |||
| a39f820ff1 | |||
| 1c621a602f | |||
| 7169f4a6cb | |||
| e202c1a40e | |||
| 0f4b4da0aa | |||
| 1f96413bd3 | |||
| 9695621c91 | |||
| 2d67f5ead6 | |||
| 29d2a45abc | |||
| 13c8b05f9a | |||
| 2c1a5359c6 | |||
| d8530f228e | |||
| 575f6c2f0a | |||
| 62cdc592c0 | |||
| 11373faf23 | |||
| 0473eec0a2 | |||
| 7fc23dc085 | |||
| c741cc06b2 | |||
| 39dbfdec82 | |||
| 0e5d6056e4 | |||
| d711993b3b | |||
| 615bf7fe43 | |||
| 424b9b5a2f | |||
| d1e494b730 | |||
| 623e4b8fff | |||
| b5dd1f2f86 | |||
| ec83f9c747 | |||
| 31af27529e | |||
| 6302565942 | |||
| cda724b2da | |||
| c2e2ba2a40 | |||
| b5d3f5faa7 | |||
| 71f3910055 | |||
| 70ed8c3b32 | |||
| af13bfc920 | |||
| e24fd92f85 | |||
| 7e27cefe6a | |||
| 450cf6424e | |||
| 54898d3dbb | |||
| dd851a2b25 | |||
| 4c6b44eb30 | |||
| 74a3efe78d | |||
| 51301fc49e | |||
| 02dd8c3dd0 | |||
| 26a778c3b2 | |||
| 9fecbd97e8 | |||
| e1383e3903 | |||
| 47532b8512 | |||
| 3c4959433a | |||
| e921b4a86a | |||
| b23b0ca239 | |||
| 191b45f054 | |||
| 15d0383349 | |||
| d2485583fd | |||
| 2b94704916 | |||
| 85ac6c215a | |||
| e83e665db9 | |||
| 645aafef16 | |||
| 152c893a6f | |||
| 7c130dda56 | |||
| 2d82dad806 | |||
| e8ac5b759d | |||
| 4833d18968 | |||
| 6eafded1f6 | |||
| 7b440b720e | |||
| e20ba7384f | |||
| 45231c6ede | |||
| 35475defb5 | |||
| 8741841f27 | |||
| 5282d19b55 | |||
| d9782aa0fb | |||
| 9751facfb4 | |||
| e0110203e7 | |||
| 088b44cc2c | |||
| 8f63bcbfbf | |||
| c8029388c9 | |||
| d9c4d847a1 | |||
| df9d9425ec | |||
| 90bb3c684e | |||
| 9c81b6de8a | |||
| 6383498041 | |||
| daeb88785d | |||
| dcea08f73b | |||
| b252b921f8 | |||
| 172826bf13 | |||
| 060f1980f5 | |||
| e223d35252 | |||
| 99dba1a4c6 | |||
| b52026c81f | |||
| 47b8c86426 | |||
| 2e55c68648 | |||
| b7362dd84d | |||
| 01637b31e1 | |||
| 0e9a39608a | |||
| 79404e4d41 | |||
| 35c21fbdaf | |||
| 8c7bd7dc11 | |||
| 09ad4f0320 | |||
| d96b836bef | |||
| 59b2ffaf95 | |||
| f1b55ddd64 | |||
| 85acac3a30 | |||
| befff5c1e5 | |||
| d72ba81a67 | |||
| fef88e2032 | |||
| 20557e8ce4 | |||
| 99c905e908 | |||
| d7b58ee2c5 | |||
| faca2d387b | |||
| 358d02d97f | |||
| b66dac7465 | |||
| f7d201859a | |||
| 61d2ef5469 | |||
| ac994b9c62 | |||
| 264dcbc331 | |||
| e5425c0ffb | |||
| e10803de68 | |||
| 07b1a0e403 | |||
| 6ed2c702d8 | |||
| 5c1c33d33e | |||
| 70d37c88b5 | |||
| 1ba37d95b5 | |||
| 0d82198849 | |||
| 39927e75f2 | |||
| e6fd33b969 | |||
| e8fe32d5af | |||
| bfc8bb864d | |||
| 9179746763 | |||
| d0177d24cb | |||
| 0573008c9c | |||
| 9506f518c2 | |||
| 0f0ae9153b | |||
| 09c7c8ac64 | |||
| 5e2dfff148 | |||
| 958b47548d | |||
| 16155ef746 | |||
| 5755b61ea6 | |||
| 353847a77f | |||
| bdf64edeb8 | |||
| b5768dd927 | |||
| 3e5abf3a4d | |||
| d3029639de | |||
| d21d7e4add | |||
| afde69b5d9 | |||
| 3319df3df0 | |||
| 1102feaac3 | |||
| deede728be | |||
| fc3dd84122 | |||
| 9239441d73 | |||
| b984811851 | |||
| 1c52446331 | |||
| b6dffa8e66 | |||
| 315d650d27 | |||
| 07c121044a | |||
| f3169afcf5 | |||
| c371fc2a8e | |||
| 6889e11fd1 | |||
| fb73fd0afc | |||
| 6fcebd7a08 | |||
| 15ea62a546 | |||
| b0cd58f5aa | |||
| 7fe8f66fd3 | |||
| 68ca99e9d9 | |||
| a2542c658b | |||
| eb203c7e62 | |||
| 6ef466f3ed | |||
| 5074246462 | |||
| 73bbcebddb | |||
| 18128303b6 | |||
| c4a2d790a3 | |||
| c1ec150696 | |||
| f4b856df15 | |||
| 85b87553dd | |||
| 5decdf3afa | |||
| a4acee4939 | |||
| d06aea2831 | |||
| ae0a8b0a33 | |||
| f0452704a1 | |||
| b8b1f1ba80 | |||
| caf7478da4 | |||
| 0e40ba78a4 | |||
| d1eac6c9eb | |||
| 8f5201b2bc | |||
| 6022001d66 | |||
| f018c367ed | |||
| 48c47f097a | |||
| 39ac215b5a | |||
| 7d562ce85c | |||
| 51b317233a | |||
| 87ce715011 | |||
| ef5afc1e23 | |||
| 486212f22a | |||
| 0e8867dd6e | |||
| ca28b5ca82 | |||
| 19e26c1759 | |||
| 790f6643a4 | |||
| 2158ad3c0b | |||
| d904d8922f | |||
| da50792500 | |||
| b4629acc48 | |||
| 0cf4118330 | |||
| dd61a6ecc3 | |||
| 8e6f1284e1 | |||
| 813d3cd492 | |||
| f421606e21 | |||
| 1ccb9183b4 | |||
| 7d9b627f37 | |||
| 3038138909 | |||
| 2ca08d21e4 | |||
| 478e96fc5f | |||
| e237c7ea1d | |||
| bf9ff088fd | |||
| e073ebedd1 | |||
| 10d4ae7dcc | |||
| 5b8bdbb3e4 | |||
| c807e21c6b | |||
| cc92d0e316 | |||
| 09c396d5a3 | |||
| bc5bbca951 | |||
| ed4faedcd7 | |||
| 251556ebed | |||
| 1324afb459 | |||
| 1119804fc2 | |||
| cdf6440197 | |||
| 8727fe00af | |||
| 7da7890bb6 | |||
| 706bd2c51f | |||
| acabec940e | |||
| 470b998b61 | |||
| 80fad05f23 | |||
| 07a912fb9a | |||
| e9d83262c4 | |||
| 74323c22f9 | |||
| 2614e89b1b | |||
| e092fe1399 | |||
| 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 | 
							
								
								
									
										20
									
								
								.clang-format
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.clang-format
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| # Format Style Options - Created with Clang Power Tools | ||||
| --- | ||||
| BasedOnStyle: WebKit | ||||
| AlignEscapedNewlines: DontAlign | ||||
| AlignOperands: DontAlign | ||||
| AllowShortCaseLabelsOnASingleLine: false | ||||
| AllowShortFunctionsOnASingleLine: false | ||||
| BreakBeforeBinaryOperators: None | ||||
| BreakBeforeBraces: Allman | ||||
| ColumnLimit: 180 | ||||
| ContinuationIndentWidth: 4 | ||||
| IndentCaseBlocks: true | ||||
| IndentWidth: 4 | ||||
| MaxEmptyLinesToKeep: 1 | ||||
| ObjCBlockIndentWidth: 4 | ||||
| ObjCBreakBeforeNestedBlockParam: false | ||||
| SortIncludes: false | ||||
| TabWidth: 4 | ||||
| UseTab: Always | ||||
| ... | ||||
							
								
								
									
										4
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| .svn | ||||
| db.sqlite | ||||
| out/**/*.o | ||||
| out/**/*.d | ||||
							
								
								
									
										2
									
								
								.git-blame-ignore-revs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.git-blame-ignore-revs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| # Add prettier to the project | ||||
| 41024ddb7961b04a5688bbc997cb74de6fab4763 | ||||
							
								
								
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| db.* | ||||
| deps/ios_toolchain/ | ||||
| deps/openssl/ | ||||
| dist/ | ||||
| .keys | ||||
| **/node_modules | ||||
| out | ||||
| *.swo | ||||
| *.swp | ||||
| .zsign_cache/ | ||||
							
								
								
									
										21
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| [submodule "deps/zlib"] | ||||
| 	path = deps/zlib | ||||
| 	url = https://github.com/madler/zlib.git | ||||
| [submodule "deps/libsodium"] | ||||
| 	path = deps/libsodium | ||||
| 	url = https://github.com/jedisct1/libsodium.git | ||||
| [submodule "deps/quickjs"] | ||||
| 	path = deps/quickjs | ||||
| 	url = https://github.com/bellard/quickjs.git | ||||
| [submodule "deps/crypt_blowfish"] | ||||
| 	path = deps/crypt_blowfish | ||||
| 	url = https://github.com/openwall/crypt_blowfish.git | ||||
| [submodule "deps/libbacktrace"] | ||||
| 	path = deps/libbacktrace | ||||
| 	url = https://github.com/ianlancetaylor/libbacktrace.git | ||||
| [submodule "deps/libuv"] | ||||
| 	path = deps/libuv | ||||
| 	url = https://github.com/libuv/libuv.git | ||||
| [submodule "deps/picohttpparser"] | ||||
| 	path = deps/picohttpparser | ||||
| 	url = https://github.com/h2o/picohttpparser.git | ||||
							
								
								
									
										14
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| node_modules | ||||
| src | ||||
| deps | ||||
| .clang-format | ||||
|  | ||||
| # Minified files | ||||
| **/*.min.css | ||||
| **/*.min.js | ||||
| **/leaflet.* | ||||
| **/commonmark* | ||||
| **/w3.css | ||||
| apps/ssb/tribute.esm.js | ||||
| apps/api/app.js | ||||
| **/emojis.json | ||||
							
								
								
									
										5
									
								
								.prettierrc.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.prettierrc.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| trailingComma: 'es5' | ||||
| useTabs: true | ||||
| semi: true | ||||
| singleQuote: true | ||||
| bracketSpacing: false | ||||
							
								
								
									
										37
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| # Contributing to Tilde Friends | ||||
|  | ||||
| Thank you for your interest in Tilde Friends. | ||||
|  | ||||
| Above all, Tilde Friends aims to be a fun, safe place to play. When that is at | ||||
| odds with the course of development, we will work through it with respectful | ||||
| communication. | ||||
|  | ||||
| ## How can I contribute? | ||||
|  | ||||
| The nature of Tilde Friends makes for a wide range of ways to contribute | ||||
|  | ||||
| - Just use it. Really, just kicking the tires will probably shake out issues | ||||
|   in useful ways at this point. | ||||
| - Report and comment on bugs: https://dev.tildefriends.net/issues. | ||||
| - Make apps. You don't need my permission to make and share apps with Tilde | ||||
|   Friends. I hope that an ecosystem of good apps grows outside of this | ||||
|   repository. If you want to recreate better versions of the stock apps, just | ||||
|   do it. If you make a better ssb app or whatever and drop me a line however | ||||
|   is most convenient for you, I will probably take a look and consider | ||||
|   replacing the stock one with it. | ||||
| - Write about it. Docs in the git repository, blog posts, private messages to | ||||
|   me with ideas...really there is no wrong answer. Just make some noise, and | ||||
|   I'll do my best to incorporate or otherwise link your feedback and make the | ||||
|   most of it. | ||||
| - Write C code in the git repository. I'm really striving for it to be the | ||||
|   case that other people don't really need to meddle in there, but if you can | ||||
|   help out, I will gladly review your pull requests via | ||||
|   https://dev.tildefriends.net/pulls. | ||||
|  | ||||
| ## Best practices | ||||
|  | ||||
| - The C code is formatted with clang-format. Run `make format`. | ||||
| - The rest is formatted with prettier. Run `npm run prettier`. | ||||
| - We strive to have code compile on all platforms with no warnings and run with | ||||
|   no sanitizer issues. | ||||
| - There are tests. Run `out/debug/tildefriends test`. | ||||
							
								
								
									
										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"] | ||||
							
								
								
									
										911
									
								
								GNUmakefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										911
									
								
								GNUmakefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,911 @@ | ||||
| .ONESHELL: | ||||
| .DELETE_ON_ERROR: | ||||
| MAKEFLAGS += --warn-undefined-variables | ||||
| MAKEFLAGS += --no-builtin-rules | ||||
|  | ||||
| VERSION_CODE := 19 | ||||
| VERSION_NUMBER := 0.0.19-wip | ||||
| VERSION_NAME := Don't let your loyalty become a burden. | ||||
|  | ||||
| SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3450300.zip | ||||
| LIBUV_URL := https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz | ||||
|  | ||||
| PROJECT = tildefriends | ||||
| BUILD_DIR ?= out | ||||
| UNAME_S := $(shell uname -s) | ||||
| UNAME_M := $(shell uname -m) | ||||
|  | ||||
| ANDROID_SDK ?= ~/Android/Sdk | ||||
|  | ||||
| HAVE_WIN := 0 | ||||
|  | ||||
| ifeq ($(UNAME_S),Darwin) | ||||
| BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease | ||||
| else ifeq ($(UNAME_S),Linux) | ||||
| BUILD_TYPES := debug release | ||||
| HAVE_ANDROID = $(if $(shell which $(ANDROID_SDK)/platform-tools/adb),1,0) | ||||
| HAVE_LINUX_IOS = $(if $(shell which deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang),1,0) | ||||
| HAVE_WIN = $(if $(shell which x86_64-w64-mingw32-gcc-win32),1,0) | ||||
| else ifeq ($(UNAME_S),Haiku) | ||||
| BUILD_TYPES := debug release | ||||
| CFLAGS += -Dstatic_assert=_Static_assert | ||||
| LDFLAGS += \ | ||||
| 	-lbsd \ | ||||
| 	-lnetwork | ||||
| else ifeq ($(UNAME_S),OpenBSD) | ||||
| BUILD_TYPES := debug release | ||||
| CFLAGS += \ | ||||
| 	-Wno-unknown-warning-option | ||||
| LDFLAGS += \ | ||||
| 	-lexecinfo \ | ||||
| 	-lc++abi | ||||
| HAVE_ANDROID := 0 | ||||
| HAVE_LINUX_IOS := 0 | ||||
| else | ||||
| $(error Unexpected host platform $(UNAME_S).) | ||||
| endif | ||||
|  | ||||
| CFLAGS += \ | ||||
| 	-std=gnu11 \ | ||||
| 	-Wall \ | ||||
| 	-Wextra \ | ||||
| 	-Wno-unused-parameter \ | ||||
| 	-MMD \ | ||||
| 	-MP \ | ||||
| 	-ffunction-sections \ | ||||
| 	-fdata-sections \ | ||||
| 	-fno-exceptions \ | ||||
| 	-g | ||||
|  | ||||
| ANDROID_MIN_SDK_VERSION := 24 | ||||
| ANDROID_TARGET_SDK_VERSION := 34 | ||||
| ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0 | ||||
| ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-$(ANDROID_TARGET_SDK_VERSION) | ||||
| ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.2.11394342 | ||||
|  | ||||
| ANDROID_ARMV7A_TARGETS := \ | ||||
| 	out/androiddebug-armv7a/tildefriends \ | ||||
| 	out/androidrelease-armv7a/tildefriends | ||||
| ANDROID_ARM64_TARGETS := \ | ||||
| 	out/androiddebug/tildefriends \ | ||||
| 	out/androidrelease/tildefriends | ||||
| ANDROID_X86_TARGETS := \ | ||||
| 	out/androiddebug-x86/tildefriends \ | ||||
| 	out/androidrelease-x86/tildefriends | ||||
| ANDROID_X86_64_TARGETS := \ | ||||
| 	out/androiddebug-x86_64/tildefriends \ | ||||
| 	out/androidrelease-x86_64/tildefriends | ||||
| ANDROID_TARGETS := \ | ||||
| 	$(ANDROID_X86_TARGETS) \ | ||||
| 	$(ANDROID_X86_64_TARGETS) \ | ||||
| 	$(ANDROID_ARMV7A_TARGETS) \ | ||||
| 	$(ANDROID_ARM64_TARGETS) | ||||
| ifeq ($(HAVE_ANDROID),1) | ||||
| BUILD_TYPES += \ | ||||
| 	androiddebug \ | ||||
| 	androidrelease \ | ||||
| 	androiddebug-armv7a \ | ||||
| 	androidrelease-armv7a \ | ||||
| 	androiddebug-x86 \ | ||||
| 	androidrelease-x86 \ | ||||
| 	androiddebug-x86_64 \ | ||||
| 	androidrelease-x86_64 | ||||
| all: out/TildeFriends-arm-debug.apk out/TildeFriends-arm-release.apk out/TildeFriends-x86-debug.apk out/TildeFriends-x86-release.apk | ||||
| endif | ||||
|  | ||||
| WINDOWS_TARGETS := \ | ||||
| 	out/windebug/tildefriends.exe \ | ||||
| 	out/winrelease/tildefriends.exe | ||||
| ifeq ($(HAVE_WIN),1) | ||||
| BUILD_TYPES += windebug winrelease | ||||
| endif | ||||
|  | ||||
| LINUX_TARGETS := \ | ||||
| 	out/debug/tildefriends \ | ||||
| 	out/release/tildefriends | ||||
| MACOS_TARGETS := \ | ||||
| 	out/macosdebug/tildefriends \ | ||||
| 	out/macosrelease/tildefriends | ||||
| IOS_TARGETS := \ | ||||
| 	out/iosdebug/tildefriends \ | ||||
| 	out/iosrelease/tildefriends | ||||
| IOSSIM_TARGETS := \ | ||||
| 	out/iossimdebug/tildefriends \ | ||||
| 	out/iossimrelease/tildefriends | ||||
| IOS_APPS = \ | ||||
| 	out/tildefriends-iosdebug.app/tildefriends \ | ||||
| 	out/tildefriends-iosrelease.app/tildefriends | ||||
| ifeq ($(HAVE_LINUX_IOS),1) | ||||
| BUILD_TYPES += iosdebug iosrelease | ||||
| all: $(IOS_APPS) | ||||
| endif | ||||
| ifeq ($(UNAME_S),Darwin) | ||||
| all: $(IOS_APPS) \ | ||||
| 	out/tildefriends-iossimdebug.app/tildefriends \ | ||||
| 	out/tildefriends-iossimrelease.app/tildefriends | ||||
| endif | ||||
|  | ||||
| DEBUG_TARGETS := \ | ||||
| 	out/debug/tildefriends \ | ||||
| 	out/windebug/tildefriends.exe \ | ||||
| 	out/iosdebug/tildefriends \ | ||||
| 	out/iossimdebug/tildefriends \ | ||||
| 	out/macosdebug/tildefriends \ | ||||
| 	out/androiddebug/tildefriends \ | ||||
| 	out/androiddebug-armv7a/tildefriends \ | ||||
| 	out/androiddebug-x86_64/tildefriends \ | ||||
| 	out/androiddebug-x86/tildefriends | ||||
| RELEASE_TARGETS := \ | ||||
| 	out/release/tildefriends \ | ||||
| 	out/winrelease/tildefriends.exe \ | ||||
| 	out/iosrelease/tildefriends \ | ||||
| 	out/iossimrelease/tildefriends \ | ||||
| 	out/macosrelease/tildefriends \ | ||||
| 	out/androidrelease/tildefriends \ | ||||
| 	out/androidrelease-armv7a/tildefriends \ | ||||
| 	out/androidrelease-x86_64/tildefriends \ | ||||
| 	out/androidrelease-x86/tildefriends | ||||
| ALL_TARGETS = $(DEBUG_TARGETS) $(RELEASE_TARGETS) | ||||
| 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),$(ALL_TARGETS)) | ||||
| NONMACOS_TARGETS := $(filter-out $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS),$(ALL_TARGETS)) | ||||
|  | ||||
| $(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer | ||||
| $(filter-out $(ANDROID_TARGETS) $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic | ||||
| $(ANDROID_TARGETS): CFLAGS += \ | ||||
| 	--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \ | ||||
| 	-fPIC \ | ||||
| 	-fdebug-compilation-dir . \ | ||||
| 	-fomit-frame-pointer \ | ||||
| 	-fno-asynchronous-unwind-tables \ | ||||
| 	-funwind-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 += -Oz | ||||
| $(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32 | ||||
| $(WINDOWS_TARGETS): AS = $(CC) | ||||
| $(WINDOWS_TARGETS): CFLAGS += \ | ||||
| 	-D_WIN32_WINNT=0x0A00 \ | ||||
| 	-DWINVER=0x0A00 \ | ||||
| 	-DNTDDI_VERSION=NTDDI_WIN10 \ | ||||
| 	-Ideps/openssl/mingw64/usr/local/include | ||||
| $(WINDOWS_TARGETS): LDFLAGS += \ | ||||
| 	-static \ | ||||
| 	-lm \ | ||||
| 	-Ldeps/openssl/mingw64/usr/local/lib | ||||
| ifeq ($(UNAME_S),Darwin) | ||||
| $(MACOS_TARGETS): CC = xcrun clang | ||||
| $(IOS_TARGETS): IOS_SYSROOT := $(shell xcrun --sdk iphoneos --show-sdk-path) | ||||
| $(IOS_TARGETS): CC = xcrun --sdk iphoneos clang -isysroot $(IOS_SYSROOT) -arch arm64 | ||||
| $(IOSSIM_TARGETS): IOSSIM_SYSROOT := $(shell xcrun --sdk iphonesimulator --show-sdk-path) | ||||
| $(IOSSIM_TARGETS): CC = xcrun --sdk iphonesimulator clang -isysroot $(IOSSIM_SYSROOT) -arch x86_64 | ||||
| else ifeq ($(UNAME_S),Linux) | ||||
| $(IOS_TARGETS): IOS_SYSROOT := deps/iPhoneOS17.0.sdk | ||||
| $(IOS_TARGETS): CC = PATH=$$PATH:deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang | ||||
| endif | ||||
| $(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android | ||||
| $(ANDROID_X86_TARGETS): ANDROID_NDK_TARGET_TRIPLE := i686-linux-android | ||||
| $(ANDROID_ARMV7A_TARGETS): ANDROID_NDK_TARGET_TRIPLE := armv7a-linux-androideabi | ||||
| $(ANDROID_ARM64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := aarch64-linux-android | ||||
| $(ANDROID_TARGETS): CC = $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/$(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION)-clang | ||||
| $(ANDROID_TARGETS): AS = $(CC) | ||||
| $(ANDROID_TARGETS): CFLAGS += \ | ||||
| 	-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \ | ||||
| 	-Wno-unknown-warning-option | ||||
| $(ANDROID_ARMV7A_TARGETS): CFLAGS += -Ideps/openssl/android/armeabi-v7a/usr/local/include | ||||
| $(ANDROID_ARMV7A_TARGETS): LDFLAGS += -Ldeps/openssl/android/armeabi-v7a/usr/local/lib | ||||
| $(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_TARGETS): CFLAGS += -Ideps/openssl/android/x86/usr/local/include | ||||
| $(ANDROID_X86_TARGETS): CFLAGS += -Wno-atomic-alignment | ||||
| $(ANDROID_X86_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86/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 | ||||
| $(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type | ||||
| $(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections | ||||
| $(IOS_TARGETS): CFLAGS += -mios-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include | ||||
| $(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib | ||||
| $(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include | ||||
| $(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/lib | ||||
|  | ||||
| ifeq ($(UNAME_M),x86_64) | ||||
| ifneq ($(UNAME_S),Haiku) | ||||
| out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common | ||||
| out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined | ||||
| endif | ||||
| endif | ||||
|  | ||||
| ifeq ($(UNAME_M),aarch64) | ||||
| out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common | ||||
| out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined | ||||
| endif | ||||
|  | ||||
| get_objs = \ | ||||
| 	$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \ | ||||
| 	$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \ | ||||
| 	$(foreach build_type,windebug winrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_win))))) \ | ||||
| 	$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androiddebug-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \ | ||||
| 	$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androidrelease-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \ | ||||
| 	$(foreach build_type,macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_macos))))) \ | ||||
| 	$(foreach build_type,iosdebug iosrelease iossimdebug iossimrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_ios))))) \ | ||||
| 	$(foreach build_type,androiddebug-x86 androidrelease-x86,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_x86))))) | ||||
|  | ||||
| APP_SOURCES := $(wildcard src/*.c) | ||||
| APP_SOURCES_ios := $(wildcard src/*.m) | ||||
| APP_OBJS := $(call get_objs,APP_SOURCES) | ||||
| $(APP_OBJS): CFLAGS += \ | ||||
| 	-Ideps/base64c/include \ | ||||
| 	-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/sqlite \ | ||||
| 	-Ideps/valgrind \ | ||||
| 	-Wdouble-promotion \ | ||||
| 	-Werror | ||||
| ifeq ($(UNAME_M),x86_64) | ||||
| $(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/macos% $(BUILD_DIR)/ios%,$(APP_OBJS)): CFLAGS += \ | ||||
| 	-fanalyzer | ||||
| endif | ||||
|  | ||||
| BLOWFISH_SOURCES := \ | ||||
| 	deps/crypt_blowfish/crypt_blowfish.c \ | ||||
| 	deps/crypt_blowfish/crypt_gensalt.c \ | ||||
| 	deps/crypt_blowfish/wrapper.c | ||||
| BLOWFISH_SOURCES_win := \ | ||||
| 	deps/crypt_blowfish/x86.S | ||||
| BLOWFISH_SOURCES_x86 := \ | ||||
| 	deps/crypt_blowfish/x86.S | ||||
| BLOWFISH_OBJS := $(call get_objs,BLOWFISH_SOURCES) | ||||
|  | ||||
| UV_SOURCES := \ | ||||
| 	deps/libuv/src/fs-poll.c \ | ||||
| 	deps/libuv/src/idna.c \ | ||||
| 	deps/libuv/src/inet.c \ | ||||
| 	deps/libuv/src/random.c \ | ||||
| 	deps/libuv/src/strscpy.c \ | ||||
| 	deps/libuv/src/strtok.c \ | ||||
| 	deps/libuv/src/threadpool.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/core.c \ | ||||
| 	deps/libuv/src/unix/dl.c \ | ||||
| 	deps/libuv/src/unix/fs.c \ | ||||
| 	deps/libuv/src/unix/getaddrinfo.c \ | ||||
| 	deps/libuv/src/unix/getnameinfo.c \ | ||||
| 	deps/libuv/src/unix/loop-watcher.c \ | ||||
| 	deps/libuv/src/unix/loop.c \ | ||||
| 	deps/libuv/src/unix/pipe.c \ | ||||
| 	deps/libuv/src/unix/poll.c \ | ||||
| 	deps/libuv/src/unix/process.c \ | ||||
| 	deps/libuv/src/unix/random-devurandom.c \ | ||||
| 	deps/libuv/src/unix/random-getrandom.c \ | ||||
| 	deps/libuv/src/unix/signal.c \ | ||||
| 	deps/libuv/src/unix/stream.c \ | ||||
| 	deps/libuv/src/unix/tcp.c \ | ||||
| 	deps/libuv/src/unix/thread.c \ | ||||
| 	deps/libuv/src/unix/tty.c \ | ||||
| 	deps/libuv/src/unix/udp.c | ||||
| ifeq ($(UNAME_S),Linux) | ||||
| 	UV_SOURCES_unix += \ | ||||
| 		deps/libuv/src/unix/linux.c \ | ||||
| 		deps/libuv/src/unix/procfs-exepath.c \ | ||||
| 		deps/libuv/src/unix/proctitle.c \ | ||||
| 		deps/libuv/src/unix/random-sysctl-linux.c | ||||
| else ifeq ($(UNAME_S),Haiku) | ||||
| 	UV_SOURCES_unix += \ | ||||
| 		deps/libuv/src/unix/bsd-ifaddrs.c \ | ||||
| 		deps/libuv/src/unix/haiku.c \ | ||||
| 		deps/libuv/src/unix/no-fsevents.c \ | ||||
| 		deps/libuv/src/unix/no-proctitle.c \ | ||||
| 		deps/libuv/src/unix/posix-hrtime.c \ | ||||
| 		deps/libuv/src/unix/posix-poll.c | ||||
| else ifeq ($(UNAME_S),OpenBSD) | ||||
| 	UV_SOURCES_unix += \ | ||||
| 		deps/libuv/src/unix/bsd-ifaddrs.c \ | ||||
| 		deps/libuv/src/unix/kqueue.c \ | ||||
| 		deps/libuv/src/unix/no-proctitle.c \ | ||||
| 		deps/libuv/src/unix/openbsd.c \ | ||||
| 		deps/libuv/src/unix/posix-hrtime.c \ | ||||
| 		deps/libuv/src/unix/random-getentropy.c | ||||
| endif | ||||
| UV_SOURCES_android := \ | ||||
| 	deps/libuv/src/unix/random-getentropy.c | ||||
| 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_SOURCES_macos := \ | ||||
| 	deps/libuv/src/unix/async.c \ | ||||
| 	deps/libuv/src/unix/bsd-ifaddrs.c \ | ||||
| 	deps/libuv/src/unix/core.c \ | ||||
| 	deps/libuv/src/unix/darwin.c \ | ||||
| 	deps/libuv/src/unix/darwin-proctitle.c \ | ||||
| 	deps/libuv/src/unix/dl.c \ | ||||
| 	deps/libuv/src/unix/fs.c \ | ||||
| 	deps/libuv/src/unix/fsevents.c \ | ||||
| 	deps/libuv/src/unix/getaddrinfo.c \ | ||||
| 	deps/libuv/src/unix/getnameinfo.c \ | ||||
| 	deps/libuv/src/unix/kqueue.c \ | ||||
| 	deps/libuv/src/unix/loop-watcher.c \ | ||||
| 	deps/libuv/src/unix/loop.c \ | ||||
| 	deps/libuv/src/unix/pipe.c \ | ||||
| 	deps/libuv/src/unix/poll.c \ | ||||
| 	deps/libuv/src/unix/process.c \ | ||||
| 	deps/libuv/src/unix/proctitle.c \ | ||||
| 	deps/libuv/src/unix/random-devurandom.c \ | ||||
| 	deps/libuv/src/unix/random-getentropy.c \ | ||||
| 	deps/libuv/src/unix/signal.c \ | ||||
| 	deps/libuv/src/unix/stream.c \ | ||||
| 	deps/libuv/src/unix/tcp.c \ | ||||
| 	deps/libuv/src/unix/thread.c \ | ||||
| 	deps/libuv/src/unix/tty.c \ | ||||
| 	deps/libuv/src/unix/udp.c | ||||
| UV_OBJS := $(call get_objs,UV_SOURCES) | ||||
| $(UV_OBJS): CFLAGS += \ | ||||
| 	-Ideps/libuv/include \ | ||||
| 	-Ideps/libuv/src \ | ||||
| 	-Wno-dangling-pointer \ | ||||
| 	-Wno-incompatible-pointer-types \ | ||||
| 	-Wno-maybe-uninitialized \ | ||||
| 	-Wno-sign-compare \ | ||||
| 	-Wno-unused-but-set-parameter \ | ||||
| 	-Wno-unused-but-set-variable \ | ||||
| 	-Wno-unused-result \ | ||||
| 	-Wno-unused-variable | ||||
| ifeq ($(UNAME_S),Linux) | ||||
| $(UV_OBJS): CFLAGS += \ | ||||
| 	-D_GNU_SOURCE | ||||
| else ifeq ($(UNAME_S),Haiku) | ||||
| $(UV_OBJS): CFLAGS += \ | ||||
| 	-D_BSD_SOURCE \ | ||||
| 	-Wno-format-truncation | ||||
| endif | ||||
|  | ||||
| SODIUM_SOURCES := \ | ||||
| 	deps/libsodium/src/libsodium/crypto_aead/aegis128l/aead_aegis128l.c \ | ||||
| 	deps/libsodium/src/libsodium/crypto_aead/aegis128l/aegis128l_soft.c \ | ||||
| 	deps/libsodium/src/libsodium/crypto_aead/aegis256/aead_aegis256.c \ | ||||
| 	deps/libsodium/src/libsodium/crypto_aead/aegis256/aegis256_soft.c \ | ||||
| 	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_core/softaes/softaes.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/sandy2x/curve25519_sandy2x.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/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 \ | ||||
| 	deps/libsodium/src/libsodium/sodium/version.c | ||||
| SODIUM_OBJS := $(call get_objs,SODIUM_SOURCES) | ||||
| $(SODIUM_OBJS): CFLAGS += \ | ||||
| 	-DCONFIGURED=1 \ | ||||
| 	-DMINIMAL=1 \ | ||||
| 	-DHAVE_ALLOCA \ | ||||
| 	-DHAVE_CPUID_V \ | ||||
| 	-DHAVE_GCC_MEMORY_FENCES \ | ||||
| 	-Wno-unused-function \ | ||||
| 	-Wno-unused-variable \ | ||||
| 	-Wno-type-limits \ | ||||
| 	-Wno-unknown-pragmas \ | ||||
| 	-Wno-attributes \ | ||||
| 	-Ideps/libsodium/builds/msvc \ | ||||
| 	-Ideps/libsodium/src/libsodium/include/sodium | ||||
| ifneq ($(UNAME_S),OpenBSD) | ||||
| $(filter-out $(BUILD_DIR)/win%,$(SODIUM_OBJS)): CFLAGS += \ | ||||
| 	-DHAVE_ALLOCA_H | ||||
| endif | ||||
|  | ||||
| SQLITE_SOURCES := deps/sqlite/sqlite3.c | ||||
| SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES) | ||||
| $(SQLITE_OBJS): CFLAGS += \ | ||||
| 	-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \ | ||||
| 	-DSQLITE_DEFAULT_MEMSTATUS=0 \ | ||||
| 	-DSQLITE_DQS=0 \ | ||||
| 	-DSQLITE_ENABLE_MEMSYS5 \ | ||||
| 	-DSQLITE_ENABLE_FTS5 \ | ||||
| 	-DSQLITE_ENABLE_JSON1 \ | ||||
| 	-DSQLITE_LIKE_DOESNT_MATCH_BLOBS \ | ||||
| 	-DSQLITE_MAX_ATTACHED=1 \ | ||||
| 	-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_SQL_LENGTH=100000 \ | ||||
| 	-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_THREADSAFE=2 \ | ||||
| 	-DSQLITE_UNTESTABLE \ | ||||
| 	-DSQLITE_USE_ALLOCA \ | ||||
| 	-DHAVE_ISNAN \ | ||||
| 	-DHAVE_GETHOSTUUID=0 \ | ||||
| 	-Wno-implicit-fallthrough \ | ||||
| 	-Wno-unused-but-set-variable \ | ||||
| 	-Wno-unused-function \ | ||||
| 	-Wno-unused-variable | ||||
|  | ||||
| QUICKJS_SOURCES := \ | ||||
| 	deps/quickjs/cutils.c \ | ||||
| 	deps/quickjs/libbf.c \ | ||||
| 	deps/quickjs/libregexp.c \ | ||||
| 	deps/quickjs/libunicode.c \ | ||||
| 	deps/quickjs/quickjs.c | ||||
| QUICKJS_OBJS := $(call get_objs,QUICKJS_SOURCES) | ||||
| $(QUICKJS_OBJS): CFLAGS += \ | ||||
| 	-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \ | ||||
| 	-DCONFIG_BIGNUM \ | ||||
| 	-D_GNU_SOURCE \ | ||||
| 	-Wno-enum-conversion \ | ||||
| 	-Wno-implicit-const-int-float-conversion \ | ||||
| 	-Wno-implicit-fallthrough \ | ||||
| 	-Wno-sign-compare \ | ||||
| 	-Wno-unused-but-set-variable \ | ||||
| 	-Wno-unused-variable | ||||
| $(NONANDROID_TARGETS): CFLAGS += -DDUMP_LEAKS | ||||
|  | ||||
| ifeq ($(UNAME_S),Haiku) | ||||
| $(QUICKJS_OBJS): CFLAGS += "-Dmalloc_usable_size(x)=0" | ||||
| else ifeq ($(UNAME_S),OpenBSD) | ||||
| $(QUICKJS_OBJS): CFLAGS += "-Dmalloc_usable_size(x)=0" | ||||
| endif | ||||
|  | ||||
| LIBBACKTRACE_SOURCES := \ | ||||
| 	deps/libbacktrace/atomic.c \ | ||||
| 	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_SOURCES_macos := \ | ||||
| 	deps/libbacktrace/dwarf.c \ | ||||
| 	deps/libbacktrace/macho.c \ | ||||
| 	deps/libbacktrace/mmap.c \ | ||||
| 	deps/libbacktrace/mmapio.c \ | ||||
| 	deps/libbacktrace/posix.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 \ | ||||
| 	-lm | ||||
| $(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \ | ||||
| 	-lssl \ | ||||
| 	-lcrypto | ||||
| ifneq ($(UNAME_S),Haiku) | ||||
| ifneq ($(UNAME_S),OpenBSD) | ||||
| debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \ | ||||
| 	-ldl | ||||
| endif | ||||
| endif | ||||
| $(WINDOWS_TARGETS): LDFLAGS += \ | ||||
| 	-lssl \ | ||||
| 	-lcrypto \ | ||||
| 	-lcrypt32 \ | ||||
| 	-ldbghelp \ | ||||
| 	-liphlpapi \ | ||||
| 	-lkernel32 \ | ||||
| 	-lole32 \ | ||||
| 	-luserenv \ | ||||
| 	-luuid \ | ||||
| 	-lws2_32 \ | ||||
| 	-lwsock32 | ||||
| $(ANDROID_TARGETS): LDFLAGS += \ | ||||
| 	-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \ | ||||
| 	-ldl \ | ||||
| 	-llog \ | ||||
| 	-lssl \ | ||||
| 	-lcrypto | ||||
| $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): CFLAGS += \ | ||||
| 	-Wno-unknown-warning-option | ||||
| $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \ | ||||
| 	-framework Foundation \ | ||||
| 	-framework CoreFoundation \ | ||||
| 	-framework UIKit \ | ||||
| 	-framework WebKit | ||||
|  | ||||
| unix: debug release | ||||
| win: windebug winrelease | ||||
| all: $(BUILD_TYPES) | ||||
| .PHONY: all win unix | ||||
|  | ||||
| ALL_APP_OBJS := \ | ||||
| 	$(APP_OBJS) \ | ||||
| 	$(BLOWFISH_OBJS) \ | ||||
| 	$(LIBBACKTRACE_OBJS) \ | ||||
| 	$(MINIUNZIP_OBJS) \ | ||||
| 	$(PICOHTTPPARSER_OBJS) \ | ||||
| 	$(QUICKJS_OBJS) \ | ||||
| 	$(SODIUM_OBJS) \ | ||||
| 	$(SQLITE_OBJS) \ | ||||
| 	$(UV_OBJS) | ||||
|  | ||||
| DEPS = $(ALL_APP_OBJS:.o=.d) | ||||
| -include $(DEPS) | ||||
|  | ||||
| define build_rules | ||||
| $(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe) | ||||
| .PHONY: $(1) | ||||
|  | ||||
| $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS)) | ||||
| 	@echo "[link] $$@" | ||||
| 	@$$(CC) -o $$@ $$^ $$(LDFLAGS) | ||||
|  | ||||
| $(BUILD_DIR)/$(1)/%.o: %.c | ||||
| 	@mkdir -p $$(dir $$@) | ||||
| 	@echo "[c] $$@" | ||||
| 	@$$(CC) $$(CFLAGS) -c $$< -o $$@ | ||||
|  | ||||
| $(BUILD_DIR)/$(1)/%.o: %.m | ||||
| 	@mkdir -p $$(dir $$@) | ||||
| 	@echo "[m] $$@" | ||||
| 	@$$(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)))) | ||||
|  | ||||
| src/version.h : $(firstword $(MAKEFILE_LIST)) | ||||
| 	@echo "[version] $@" | ||||
| 	@echo "#define VERSION_NUMBER \"$(VERSION_NUMBER)\"" > $@ | ||||
| 	@echo "#define VERSION_NAME \"$(VERSION_NAME)\"" >> $@ | ||||
|  | ||||
| src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST)) | ||||
| 	@echo "[android_version] $@" | ||||
| 	@sed -i \ | ||||
| 		-e 's/versionCode=".*"/versionCode="$(VERSION_CODE)"/' \ | ||||
| 		-e 's/versionName=".*"/versionName="$(VERSION_NUMBER)"/' \ | ||||
| 		-e 's/android:minSdkVersion="[[:digit:]]*"/android:minSdkVersion="$(ANDROID_MIN_SDK_VERSION)"/' \ | ||||
| 		-e 's/android:targetSdkVersion="[[:digit:]]*"/android:targetSdkVersion="$(ANDROID_TARGET_SDK_VERSION)"/' \ | ||||
| 		$@ | ||||
|  | ||||
| # Android support. | ||||
| out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml | ||||
| 	@mkdir -p $(dir $@) | ||||
| 	@echo "[aapt2] $@" | ||||
| 	@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml | ||||
|  | ||||
| out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml | ||||
| 	@mkdir -p $(dir $@) | ||||
| 	@echo "[aapt2] $@" | ||||
| 	@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml | ||||
|  | ||||
| out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.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 out/res/drawable_icon.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 -encoding UTF-8 -Xlint:deprecation -XDuseUnsharedTable=true -classpath $(ANDROID_PLATFORM)/android.jar:$(ANDROID_BUILD_TOOLS)/core-lambda-stubs.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/prettier/ \ | ||||
| 	deps/lit/ | ||||
|  | ||||
| RAW_FILES := $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f)) | ||||
|  | ||||
| out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug | ||||
| out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release | ||||
| out/apk/TildeFriends-x86-debug.unsigned.apk: BUILD_TYPE := debug | ||||
| out/apk/TildeFriends-x86-release.unsigned.apk: BUILD_TYPE := release | ||||
|  | ||||
| out/apk/TildeFriends-arm-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk | ||||
| out/apk/TildeFriends-arm-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk | ||||
| out/apk/TildeFriends-x86-debug.unsigned.apk: out/apk/classes.dex out/androiddebug-x86_64/tildefriends out/androiddebug-x86/tildefriends $(RAW_FILES) out/apk/res.apk | ||||
| out/apk/TildeFriends-x86-release.unsigned.apk: out/apk/classes.dex out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.apk | ||||
|  | ||||
| out/apk/TildeFriends-arm-%.unsigned.apk: | ||||
| 	@mkdir -p $(dir $@) out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/ | ||||
| 	@echo "[aapt] $@" | ||||
| 	@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so | ||||
| 	@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so | ||||
| 	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so | ||||
| 	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so | ||||
| 	@cp out/apk/res.apk $@.zip | ||||
| 	@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/ | ||||
| 	@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../ | ||||
| 	@zip -u $@.zip -q -9 $(RAW_FILES) | ||||
| 	@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@ | ||||
|  | ||||
| out/apk/TildeFriends-x86-%.unsigned.apk: | ||||
| 	@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/ | ||||
| 	@echo "[aapt] $@" | ||||
| 	@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so | ||||
| 	@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so | ||||
| 	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so | ||||
| 	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so | ||||
| 	@cp out/apk/res.apk $@.zip | ||||
| 	@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/ | ||||
| 	@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../ | ||||
| 	@zip -u $@.zip -q -9 $(RAW_FILES) | ||||
| 	@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@ | ||||
|  | ||||
| out/%.apk: out/apk/%.unsigned.apk | ||||
| 	@echo "[apksigner] $(notdir $@)" | ||||
| 	@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $< | ||||
|  | ||||
| out/%.zopfli.apk: out/%.apk | ||||
| 	@echo "[zopfli] $(notdir $@)" | ||||
| 	$(ANDROID_BUILD_TOOLS)/zipalign -f -z 4 $< $@.zopfli | ||||
| 	@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $@.zopfli | ||||
|  | ||||
| release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk | ||||
| .PHONY: release-apk | ||||
|  | ||||
| releaseapkgo: out/TildeFriends-arm-release.apk | ||||
| 	@adb install -r $< | ||||
| 	@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity | ||||
| .PHONY: releaseapkgo | ||||
|  | ||||
| # iOS Support | ||||
| out/%.app/Info.plist: src/ios/Info.plist | ||||
| 	@mkdir -p $(dir $@) | ||||
| 	@cp -v $< $@ | ||||
| out/%.app/tildefriends.png: src/ios/tildefriends.png | ||||
| 	@mkdir -p $(dir $@) | ||||
| 	@cp -v $< $@ | ||||
|  | ||||
| out/data.zip: $(RAW_FILES) | ||||
| 	@zip -u $@ -q -9 $(RAW_FILES) | ||||
|  | ||||
| out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip | ||||
| 	@mkdir -p $(dir $@) | ||||
| 	@cp -v $< $@ | ||||
| ifeq ($(HAVE_LINUX_IOS),1) | ||||
| 	@zsign -q -k .keys/apple.p12 -f -m src/ios/embedded.mobileprovision $(realpath $(dir $@)) | ||||
| endif | ||||
| .SECONDARY: | ||||
| out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends | ||||
| 	@echo "[ipa] $@" | ||||
| 	@rm -rf $@.tmp $@ | ||||
| 	@mkdir -p $@.tmp/Payload/tildefriends.app/ | ||||
| 	@cp -R $(dir $<)/* $@.tmp/Payload/tildefriends.app/ | ||||
| 	@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./ | ||||
| 	@rm -rf $@.tmp/ | ||||
|  | ||||
|  | ||||
| out/%/tildefriends.standalone: out/%/tildefriends out/data.zip | ||||
| 	@echo "[standalone] $@" | ||||
| 	@cat $< out/data.zip > $@ | ||||
| 	@chmod +x $@ | ||||
| out/%/tildefriends.standalone.exe: out/%/tildefriends.exe out/data.zip | ||||
| 	@echo "[standalone] $@" | ||||
| 	@cat $< out/data.zip > $@ | ||||
| 	@chmod +x $@ | ||||
|  | ||||
| iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends | ||||
| iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends | ||||
| iosdebug-app: out/tildefriends-iosdebug.app/tildefriends | ||||
| iosrelease-app: out/tildefriends-iosrelease.app/tildefriends | ||||
|  | ||||
| iosdebug-ipa: out/tildefriends-debug.ipa | ||||
| iosrelease-ipa: out/tildefriends-release.ipa | ||||
| .PHONY: iossimdebug-app iossimrelease-app iosdebug-app iosrelease-app | ||||
|  | ||||
| ios%go: out/tildefriends-ios%.app/tildefriends | ||||
| 	ideviceinstaller -i $(realpath $(dir $<)) | ||||
|  | ||||
| iossimdebuggo: out/tildefriends-iossimdebug.app/tildefriends | ||||
| 	xcrun simctl install booted out/tildefriends-iossimdebug.app/ | ||||
| 	xcrun simctl launch booted com.unprompted.tildefriends | ||||
| .PHONY: iossimdebuggo | ||||
|  | ||||
| apklog: | ||||
| 	@adb logcat *:S tildefriends | ||||
| .PHONY: apklog | ||||
|  | ||||
| fetchdeps: | ||||
| 	@echo "[fetch] libuv" | ||||
| 	@test -f out/deps/libuv.tar.gz && test "$$(cat out/deps/libuv.txt 2>/dev/null)" = $(LIBUV_URL) || (mkdir -p out/deps/ && curl -q $(LIBUV_URL) -o out/deps/libuv.tar.gz) | ||||
| 	@test -d deps/libuv/ && test "$$(cat out/deps/libuv.txt 2>/dev/null)" = $(LIBUV_URL) || (rm -rf deps/libuv/ && mkdir -p deps/libuv/ && tar -C deps/libuv/ -m --strip=1 -xf out/deps/libuv.tar.gz) | ||||
| 	@echo -n $(LIBUV_URL) > out/deps/libuv.txt | ||||
| 	@echo "[fetch] sqlite" | ||||
| 	@test -f out/deps/sqlite.zip && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p out/deps/ && curl -q $(SQLITE_URL) -o out/deps/sqlite.zip) | ||||
| 	@test -d deps/sqlite/ && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p deps/sqlite/ && unzip -qDjo -d deps/sqlite/ out/deps/sqlite.zip) | ||||
| 	@echo -n $(SQLITE_URL) > out/deps/sqlite.txt | ||||
| 	@echo "[fetch] prettier" | ||||
| 	@test -f deps/prettier/standalone.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/standalone.mjs | ||||
| 	@test -f deps/prettier/html.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/html.mjs | ||||
| 	@test -f deps/prettier/babel.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/babel.mjs | ||||
| 	@test -f deps/prettier/estree.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/estree.mjs | ||||
| .PHONY: fetchdeps | ||||
|  | ||||
| ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a | ||||
| $(ANDROID_DEPS): | ||||
| 	+@tools/ssl-android | ||||
| $(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS) | ||||
|  | ||||
| ifeq ($(HAVE_WIN),1) | ||||
| WINDOWS_DEPS := deps/openssl/mingw64/usr/local/lib/libssl.a | ||||
| $(WINDOWS_DEPS): | ||||
| 	+@tools/ssl-mingw64 | ||||
| $(filter $(BUILD_DIR)/win%,$(APP_OBJS)): | $(WINDOWS_DEPS) | ||||
| endif | ||||
|  | ||||
| ifeq ($(UNAME_S),Darwin) | ||||
| IOS_DEPS := deps/openssl/ios/ios64-xcrun/usr/local/lib/libssl.a | ||||
| $(IOS_DEPS): | ||||
| 	+@tools/ssl-ios | ||||
| $(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS) | ||||
| endif | ||||
|  | ||||
| clean: | ||||
| 	rm -rf $(BUILD_DIR) | ||||
| .PHONY: clean | ||||
|  | ||||
| dist: release-apk iosrelease-ipa $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) | ||||
| 	@echo [archive] dist/tildefriends-$(VERSION_NUMBER).tar.xz | ||||
| 	@rm -rf out/tildefriends-$(VERSION_NUMBER) | ||||
| 	@mkdir -p dist/ out/tildefriends-$(VERSION_NUMBER) | ||||
| 	@git ls-files --recurse-submodules | tar -c -T- | tar -x -C out/tildefriends-$(VERSION_NUMBER) | ||||
| 	@tar \ | ||||
| 		--exclude=apps/welcome* \ | ||||
| 		--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \ | ||||
| 		--exclude=deps/libsodium/builds/msvc/vs* \ | ||||
| 		--exclude=deps/libsodium/builds/msvc/build \ | ||||
| 		--exclude=deps/libsodium/builds/msvc/properties \ | ||||
| 		--exclude=deps/libsodium/configure \ | ||||
| 		--exclude=deps/libsodium/test \ | ||||
| 		--exclude=deps/libuv/docs \ | ||||
| 		--exclude=deps/libuv/test \ | ||||
| 		--exclude=deps/openssl \ | ||||
| 		--exclude=deps/speedscope/*.map \ | ||||
| 		--exclude=deps/sqlite/shell.c \ | ||||
| 		--exclude=deps/zlib/contrib/vstudio \ | ||||
| 		--exclude=deps/zlib/doc \ | ||||
| 		-caf dist/tildefriends-$(VERSION_NUMBER).tar.xz \ | ||||
| 		-C out/ \ | ||||
| 		tildefriends-$(VERSION_NUMBER) | ||||
| 	@echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk" | ||||
| 	@cp out/TildeFriends-x86-release.zopfli.apk dist/TildeFriends-x86-$(VERSION_NUMBER).apk | ||||
| 	@echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk" | ||||
| 	@cp out/TildeFriends-arm-release.zopfli.apk dist/TildeFriends-arm-$(VERSION_NUMBER).apk | ||||
| 	@echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa" | ||||
| 	@cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa | ||||
| 	@test $(HAVE_WIN) && echo "[cp] tildefriends-$(VERSION_NUMBER).exe" | ||||
| 	@test $(HAVE_WIN) && cp out/winrelease/tildefriends.standalone.exe dist/tildefriends-$(VERSION_NUMBER).exe | ||||
| .PHONY: dist | ||||
|  | ||||
| dist-test: dist | ||||
| 	@tar -xf tildefriends-$(VERSION_NUMBER).tar.xz | ||||
| 	@$(MAKE) -C tildefriends-$(VERSION_NUMBER)/ debug release | ||||
| 	@docker build tildefriends-$(VERSION_NUMBER)/ | ||||
| 	@rm -rf tildefriends-$(VERSION_NUMBER) | ||||
| .PHONY: dist-test | ||||
|  | ||||
| format: | ||||
| 	@clang-format -i $(wildcard src/*.c src/*.h src/*.m) | ||||
| .PHONY: format | ||||
|  | ||||
| prettier: | ||||
| 	@npm run prettier | ||||
| .PHONY: prettier | ||||
|  | ||||
| docs: | ||||
| 	@doxygen | ||||
| .PHONY: docs | ||||
							
								
								
									
										680
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										680
									
								
								LICENSE
									
									
									
									
									
								
							| @@ -1,661 +1,19 @@ | ||||
|                     GNU AFFERO GENERAL PUBLIC LICENSE | ||||
|                        Version 3, 19 November 2007 | ||||
|  | ||||
|  Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> | ||||
|  Everyone is permitted to copy and distribute verbatim copies | ||||
|  of this license document, but changing it is not allowed. | ||||
|  | ||||
|                             Preamble | ||||
|  | ||||
|   The GNU Affero General Public License is a free, copyleft license for | ||||
| software and other kinds of works, specifically designed to ensure | ||||
| cooperation with the community in the case of network server software. | ||||
|  | ||||
|   The licenses for most software and other practical works are designed | ||||
| to take away your freedom to share and change the works.  By contrast, | ||||
| our General Public Licenses are intended to guarantee your freedom to | ||||
| share and change all versions of a program--to make sure it remains free | ||||
| software for all its users. | ||||
|  | ||||
|   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/>. | ||||
| Copyright 2014 Cory McWilliams | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy of | ||||
| this software and associated documentation files (the "Software"), to deal in | ||||
| the Software without restriction, including without limitation the rights to | ||||
| 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 | ||||
| so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
|   | ||||
							
								
								
									
										170
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										170
									
								
								Makefile
									
									
									
									
									
								
							| @@ -1,170 +0,0 @@ | ||||
| PROJECT = tildefriends | ||||
| BUILD_DIR ?= out | ||||
|  | ||||
| COMMON_CFLAGS = \ | ||||
| 	-Wall \ | ||||
| 	-Werror \ | ||||
| 	-Wextra \ | ||||
| 	-Wno-unused-parameter \ | ||||
| 	-Wno-cast-function-type \ | ||||
| 	-MMD \ | ||||
| 	-ffunction-sections \ | ||||
| 	-fdata-sections | ||||
| COMMON_LDFLAGS += -Wl,-gc-sections | ||||
|  | ||||
| ifneq ($(UNUSED),) | ||||
| 	COMMON_LDFLAGS += -Wl,-print-gc-sections | ||||
| endif | ||||
|  | ||||
| ifneq ($(DEBUG),) | ||||
| 	COMMON_CFLAGS += -g -fsanitize=address -fsanitize=undefined | ||||
| 	COMMON_LDFLAGS += -fsanitize=address -fsanitize=undefined | ||||
| 	BUILD_DIR := $(BUILD_DIR)/debug | ||||
| else | ||||
| 	COMMON_CFLAGS += -DNDEBUG -O3 | ||||
| 	BUILD_DIR := $(BUILD_DIR)/release | ||||
| endif | ||||
|  | ||||
| APP_BIN = $(BUILD_DIR)/$(PROJECT) | ||||
| APP_SOURCES = $(wildcard src/*.c) | ||||
| APP_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(APP_SOURCES)) | ||||
| $(APP_OBJS): CFLAGS += \ | ||||
| 	-Ideps/base64c/include \ | ||||
| 	-Ideps/crypt_blowfish \ | ||||
| 	-Ideps/quickjs \ | ||||
| 	-Ideps/sqlite \ | ||||
| 	-Ideps/libuv/include \ | ||||
| 	-Ideps/xopt | ||||
|  | ||||
| BASE64C_SOURCES = deps/base64c/src/base64c.c | ||||
| 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_gensalt.c \ | ||||
| 	deps/crypt_blowfish/wrapper.c | ||||
| BLOWFISH_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(BLOWFISH_SOURCES)) | ||||
|  | ||||
| UV_SOURCES = \ | ||||
| 	deps/libuv/src/fs-poll.c \ | ||||
| 	deps/libuv/src/idna.c \ | ||||
| 	deps/libuv/src/inet.c \ | ||||
| 	deps/libuv/src/random.c \ | ||||
| 	deps/libuv/src/strscpy.c \ | ||||
| 	deps/libuv/src/threadpool.c \ | ||||
| 	deps/libuv/src/timer.c \ | ||||
| 	deps/libuv/src/unix/async.c \ | ||||
| 	deps/libuv/src/unix/core.c \ | ||||
| 	deps/libuv/src/unix/dl.c \ | ||||
| 	deps/libuv/src/unix/fs.c \ | ||||
| 	deps/libuv/src/unix/getaddrinfo.c \ | ||||
| 	deps/libuv/src/unix/getnameinfo.c \ | ||||
| 	deps/libuv/src/unix/linux-core.c \ | ||||
| 	deps/libuv/src/unix/linux-inotify.c \ | ||||
| 	deps/libuv/src/unix/linux-syscalls.c \ | ||||
| 	deps/libuv/src/unix/loop-watcher.c \ | ||||
| 	deps/libuv/src/unix/loop.c \ | ||||
| 	deps/libuv/src/unix/pipe.c \ | ||||
| 	deps/libuv/src/unix/poll.c \ | ||||
| 	deps/libuv/src/unix/process.c \ | ||||
| 	deps/libuv/src/unix/procfs-exepath.c \ | ||||
| 	deps/libuv/src/unix/proctitle.c \ | ||||
| 	deps/libuv/src/unix/random-devurandom.c \ | ||||
| 	deps/libuv/src/unix/random-getrandom.c \ | ||||
| 	deps/libuv/src/unix/random-sysctl-linux.c \ | ||||
| 	deps/libuv/src/unix/signal.c \ | ||||
| 	deps/libuv/src/unix/stream.c \ | ||||
| 	deps/libuv/src/unix/tcp.c \ | ||||
| 	deps/libuv/src/unix/thread.c \ | ||||
| 	deps/libuv/src/unix/tty.c \ | ||||
| 	deps/libuv/src/unix/udp.c \ | ||||
| 	deps/libuv/src/uv-common.c \ | ||||
| 	deps/libuv/src/uv-data-getter-setters.c \ | ||||
| 	deps/libuv/src/version.c | ||||
| UV_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(UV_SOURCES)) | ||||
| $(UV_OBJS): CFLAGS += \ | ||||
| 	-Ideps/libuv/include \ | ||||
| 	-Ideps/libuv/src \ | ||||
| 	-Wno-unused-but-set-variable \ | ||||
| 	-Wno-incompatible-pointer-types \ | ||||
| 	-Wno-sign-compare \ | ||||
| 	-D_GNU_SOURCE \ | ||||
|  | ||||
| SQLITE_SOURCES = deps/sqlite/sqlite3.c | ||||
| SQLITE_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(SQLITE_SOURCES)) | ||||
| $(SQLITE_OBJS): CFLAGS += \ | ||||
| 	-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \ | ||||
| 	-DSQLITE_ENABLE_JSON1 \ | ||||
| 	-DSQLITE_MAX_LENGTH=5242880 \ | ||||
| 	-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_LIKE_PATTERN_LENGTH=50 \ | ||||
| 	-DSQLITE_MAX_VARIABLE_NUMBER=100 \ | ||||
| 	-DSQLITE_MAX_TRIGGER_DEPTH=10 \ | ||||
| 	-DSQLITE_SECURE_DELETE \ | ||||
| 	-Wno-implicit-fallthrough | ||||
|  | ||||
| XOPT_SOURCES = deps/xopt/xopt.c | ||||
| XOPT_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(XOPT_SOURCES)) | ||||
|  | ||||
| QUICKJS_SOURCES = \ | ||||
| 	deps/quickjs/cutils.c \ | ||||
| 	deps/quickjs/libbf.c \ | ||||
| 	deps/quickjs/libregexp.c \ | ||||
| 	deps/quickjs/libunicode.c \ | ||||
| 	deps/quickjs/quickjs-libc.c \ | ||||
| 	deps/quickjs/quickjs.c | ||||
| QUICKJS_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(QUICKJS_SOURCES)) | ||||
| $(QUICKJS_OBJS): CFLAGS += \ | ||||
| 	-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \ | ||||
| 	-DDUMP_LEAKS \ | ||||
| 	-D_GNU_SOURCE \ | ||||
| 	-Wno-sign-compare \ | ||||
| 	-Wno-implicit-fallthrough \ | ||||
| 	-Wno-unused-variable \ | ||||
| 	-Wno-unused-but-set-variable | ||||
|  | ||||
| APP_LDFLAGS = \ | ||||
| 	$(COMMON_LDFLAGS) \ | ||||
| 	$(LDFLAGS) \ | ||||
| 	-pthread \ | ||||
| 	-ldl \ | ||||
| 	-lm \ | ||||
| 	-lssl \ | ||||
| 	-lcrypto \ | ||||
| 	-lsodium | ||||
|  | ||||
| DEFAULT_TARGET = $(APP_BIN) | ||||
| all: $(DEFAULT_TARGET) | ||||
| .PHONY: all | ||||
|  | ||||
| ALL_APP_OBJS = \ | ||||
| 	$(APP_OBJS) \ | ||||
| 	$(BASE64C_OBJS) \ | ||||
| 	$(BLOWFISH_OBJS) \ | ||||
| 	$(UV_OBJS) \ | ||||
| 	$(SQLITE_OBJS) \ | ||||
| 	$(QUICKJS_OBJS) \ | ||||
| 	$(XOPT_OBJS) | ||||
|  | ||||
| DEPS = $(ALL_APP_OBJS:.o=.d) | ||||
| -include $(DEPS) | ||||
|  | ||||
| $(APP_BIN): $(ALL_APP_OBJS) | ||||
| 	$(CC) -o $@ $^ $(APP_LDFLAGS) | ||||
|  | ||||
| $(BUILD_DIR)/%.o: %.c | ||||
| 	@mkdir -p $(dir $@) | ||||
| 	@echo [c] $@ | ||||
| 	@$(CC) $(COMMON_CFLAGS) $(CFLAGS) -c $< -o $@ | ||||
|  | ||||
| clean: | ||||
| 	rm -rf $(BUILD_DIR) | ||||
| .PHONY: clean | ||||
							
								
								
									
										47
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,26 +1,49 @@ | ||||
| # 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. | ||||
|  | ||||
| A public instance lives at https://www.tildefriends.net/. | ||||
|  | ||||
| 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 | ||||
|  | ||||
| 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. | ||||
| 3. Make creating and sharing web applications accessible to anyone with a browser. | ||||
| 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. | ||||
|  | ||||
| ## Building | ||||
| 1. Requires libsodium and openssl.  Other dependencies are kept up to date in the tree. | ||||
| 2. To build, run `make` or `make DEBUG=1`.  An executable will be generated in a subdirectory of `out/`. | ||||
|  | ||||
| Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for | ||||
| all of those host platforms plus mingw64, iOS, and android. | ||||
|  | ||||
| 1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies | ||||
|    are kept up to date in the tree. | ||||
| 2. To build, run `make debug` or `make release`. An executable will be | ||||
|    generated in a subdirectory of `out/`. | ||||
| 3. It's possible to build for Android, iOS, and Windows on Linux, if you have | ||||
|    the right dependencies in the right places. `make windebug winrelease | ||||
| iosdebug-ipa iosrelease-ipa release-apk`. | ||||
| 4. To build in docker, `docker build .`. | ||||
| 5. `make format` will normalize formatting to the coding standard. | ||||
|  | ||||
| ## Running | ||||
| This is only just starting to show some signs of beginning to work as intended.  Set expectations low. | ||||
|  | ||||
| Running the built `tildefriends` executable will start a web server at <http://localhost:12345/>.  `tildefriends -h` lists further options. | ||||
| By default, 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.  Everything can be managed entirely from the web interface. | ||||
|  | ||||
| Some starter apps can be installed by running `tildefriends import -u cory`.  Hint: `~cory/docs/` and `~cory/index/`. | ||||
| The first user to create an account and log in will be granted administrative | ||||
| privileges. Further administration can be done at | ||||
| <http://localhost:12345/~core/admin/>. | ||||
|  | ||||
| ## 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/>. | ||||
|  | ||||
| Docs are a work in progress: | ||||
| <https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>. | ||||
|  | ||||
| ## 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. | ||||
|   | ||||
							
								
								
									
										5
									
								
								apps/admin.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/admin.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "🎛", | ||||
| 	"previous": "&vrpS/vE7n588iYv1p8HafDxHB+YDHTrtUbJiu9nGA9I=.sha256" | ||||
| } | ||||
							
								
								
									
										30
									
								
								apps/admin/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								apps/admin/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| 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() { | ||||
| 	try { | ||||
| 		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)) | ||||
| 		); | ||||
| 	} catch { | ||||
| 		await app.setDocument( | ||||
| 			'<span style="color: #f00">Only an administrator can modify these settings.</span>' | ||||
| 		); | ||||
| 	} | ||||
| } | ||||
| main(); | ||||
							
								
								
									
										40
									
								
								apps/admin/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								apps/admin/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| <!doctype html> | ||||
| <html style="width: 100%"> | ||||
| 	<head> | ||||
| 		<script> | ||||
| 			const g_data = $data; | ||||
| 		</script> | ||||
| 		<link rel="stylesheet" href="w3.css"></link> | ||||
| 		<style> | ||||
| 			/* 2018 Valiant Poppy */ | ||||
| 			.w3-theme-l5 {color:#000 !important; background-color:#fbf3f3 !important} | ||||
| 			.w3-theme-l4 {color:#000 !important; background-color:#f3d7d6 !important} | ||||
| 			.w3-theme-l3 {color:#000 !important; background-color:#e6afae !important} | ||||
| 			.w3-theme-l2 {color:#fff !important; background-color:#da8785 !important} | ||||
| 			.w3-theme-l1 {color:#fff !important; background-color:#cd5f5d !important} | ||||
| 			.w3-theme-d1 {color:#fff !important; background-color:#a93634 !important} | ||||
| 			.w3-theme-d2 {color:#fff !important; background-color:#96302e !important} | ||||
| 			.w3-theme-d3 {color:#fff !important; background-color:#832a28 !important} | ||||
| 			.w3-theme-d4 {color:#fff !important; background-color:#702423 !important} | ||||
| 			.w3-theme-d5 {color:#fff !important; background-color:#5e1e1d !important} | ||||
|  | ||||
| 			.w3-theme-light {color:#000 !important; background-color:#fbf3f3 !important} | ||||
| 			.w3-theme-dark {color:#fff !important; background-color:#5e1e1d !important} | ||||
| 			.w3-theme-action {color:#fff !important; background-color:#5e1e1d !important} | ||||
|  | ||||
| 			.w3-theme {color:#fff !important; background-color:#bd3d3a !important} | ||||
| 			.w3-text-theme {color:#bd3d3a !important} | ||||
| 			.w3-border-theme {border-color:#bd3d3a !important} | ||||
|  | ||||
| 			.w3-hover-theme:hover {color:#fff !important; background-color:#bd3d3a !important} | ||||
| 			.w3-hover-text-theme:hover {color:#bd3d3a !important} | ||||
| 			.w3-hover-border-theme:hover {border-color:#bd3d3a !important} | ||||
| 		</style> | ||||
| 	</head> | ||||
| 	<body class="w3-theme-l4"> | ||||
| 		<header class="w3-row w3-padding w3-header w3-theme-l1"> | ||||
| 			<h1>Tilde Friends Administration</h1> | ||||
| 		</header> | ||||
| 	</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
											
										
									
								
							
							
								
								
									
										87
									
								
								apps/admin/script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								apps/admin/script.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| 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` | ||||
| 				<li class="w3-row"> | ||||
| 					<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${key}</label> | ||||
| 					<div class="w3-quarter w3-padding">${description.description}</div> | ||||
| 					<input class="w3-quarter w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input> | ||||
| 					<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button> | ||||
| 				</li> | ||||
| 			`; | ||||
| 		} else if (description.type === 'textarea') { | ||||
| 			return html` | ||||
| 				<li class="w3-row"> | ||||
| 					<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${key}</label> | ||||
| 					<div class="w3-rest w3-padding">${description.description}</div> | ||||
| 					<textarea class="w3-input" style="vertical-align: top; resize: vertical" id=${'gs_' + key}>${description.value}</textarea> | ||||
| 					<button class="w3-button w3-right w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button> | ||||
| 				</li> | ||||
| 			`; | ||||
| 		} else { | ||||
| 			return html` | ||||
| 				<li class="w3-row"> | ||||
| 					<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${key}</label> | ||||
| 					<div class="w3-quarter w3-padding">${description.description}</div> | ||||
| 					<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input> | ||||
| 					<button class="w3-button w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button> | ||||
| 				</li> | ||||
| 			`; | ||||
| 		} | ||||
| 	} | ||||
| 	const user_template = (user, permissions) => html` | ||||
| 		<li class="w3-card w3-margin"> | ||||
| 			<button class="w3-button w3-theme-action" @click=${(e) => delete_user(user)}>Delete</button> | ||||
| 			${user}: ${permissions.map((x) => permission_template(x))} | ||||
| 		</li> | ||||
| 	`; | ||||
| 	const users_template = (users) => | ||||
| 		html` | ||||
| 			<header class="w3-container w3-theme-l2"><h2>Users</h2></header> | ||||
| 			<ul class="w3-ul"> | ||||
| 				${Object.entries(users).map((u) => user_template(u[0], u[1]))} | ||||
| 			</ul>`; | ||||
| 	const page_template = (data) => | ||||
| 		html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%"> | ||||
| 			<header class="w3-container w3-theme-l2"><h2>Global Settings</h2></header> | ||||
| 			<div class="w3-container"> | ||||
| 				<ul class="w3-ul"> | ||||
| 					${Object.keys(data.settings) | ||||
| 						.sort() | ||||
| 						.map((x) => html`${input_template(x, data.settings[x])}`)} | ||||
| 				</ul> | ||||
| 			</div> | ||||
| 			${users_template(data.users)} | ||||
| 		</div> `; | ||||
| 	render(page_template(g_data), document.body); | ||||
| }); | ||||
							
								
								
									
										235
									
								
								apps/admin/w3.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								apps/admin/w3.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,235 @@ | ||||
| /* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */ | ||||
| html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit} | ||||
| /* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ | ||||
| html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} | ||||
| article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item} | ||||
| audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline} | ||||
| audio:not([controls]){display:none;height:0}[hidden],template{display:none} | ||||
| a{background-color:transparent}a:active,a:hover{outline-width:0} | ||||
| abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted} | ||||
| b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000} | ||||
| small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} | ||||
| sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none} | ||||
| code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible} | ||||
| button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold} | ||||
| button,input{overflow:visible}button,select{text-transform:none} | ||||
| button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button} | ||||
| button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0} | ||||
| button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText} | ||||
| fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em} | ||||
| legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto} | ||||
| [type=checkbox],[type=radio]{padding:0} | ||||
| [type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto} | ||||
| [type=search]{-webkit-appearance:textfield;outline-offset:-2px} | ||||
| [type=search]::-webkit-search-decoration{-webkit-appearance:none} | ||||
| ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit} | ||||
| /* End extract */ | ||||
| html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden} | ||||
| h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px} | ||||
| .w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace} | ||||
| h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px} | ||||
| hr{border:0;border-top:1px solid #eee;margin:20px 0} | ||||
| .w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit} | ||||
| .w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc} | ||||
| .w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1} | ||||
| .w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1} | ||||
| .w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center} | ||||
| .w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top} | ||||
| .w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px} | ||||
| .w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap} | ||||
| .w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)} | ||||
| .w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}    | ||||
| .w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none} | ||||
| .w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none} | ||||
| .w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%} | ||||
| .w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none} | ||||
| .w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block} | ||||
| .w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s} | ||||
| .w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%} | ||||
| .w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc} | ||||
| .w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer} | ||||
| .w3-dropdown-hover:hover .w3-dropdown-content{display:block} | ||||
| .w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000} | ||||
| .w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000} | ||||
| .w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1} | ||||
| .w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px} | ||||
| .w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto} | ||||
| .w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%} | ||||
| .w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%} | ||||
| .w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px} | ||||
| .w3-main,#main{transition:margin-left .4s} | ||||
| .w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)} | ||||
| .w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px} | ||||
| .w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto} | ||||
| .w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0} | ||||
| .w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left} | ||||
| .w3-bar .w3-button{white-space:normal} | ||||
| .w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0} | ||||
| .w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%} | ||||
| .w3-responsive{display:block;overflow-x:auto} | ||||
| .w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before, | ||||
| .w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both} | ||||
| .w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%} | ||||
| .w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%} | ||||
| .w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%} | ||||
| .w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%} | ||||
| @media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%} | ||||
| .w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%} | ||||
| .w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}} | ||||
| @media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%} | ||||
| .w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%} | ||||
| .w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}} | ||||
| .w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px} | ||||
| .w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px} | ||||
| .w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell} | ||||
| .w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom} | ||||
| .w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important} | ||||
| @media (max-width:1205px){.w3-auto{max-width:95%}} | ||||
| @media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px} | ||||
| .w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}	 | ||||
| .w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center} | ||||
| .w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}} | ||||
| @media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}} | ||||
| @media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}} | ||||
| @media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}} | ||||
| @media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}} | ||||
| .w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0} | ||||
| .w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2} | ||||
| .w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0} | ||||
| .w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0} | ||||
| .w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)} | ||||
| .w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)} | ||||
| .w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)} | ||||
| .w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} | ||||
| .w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} | ||||
| .w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none} | ||||
| .w3-display-position{position:absolute} | ||||
| .w3-circle{border-radius:50%} | ||||
| .w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px} | ||||
| .w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px} | ||||
| .w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px} | ||||
| .w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px} | ||||
| .w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word} | ||||
| .w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%} | ||||
| .w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)} | ||||
| .w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)} | ||||
| .w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}} | ||||
| .w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} | ||||
| .w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}} | ||||
| .w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}} | ||||
| .w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}} | ||||
| .w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}} | ||||
| .w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}} | ||||
| .w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}} | ||||
| .w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important} | ||||
| .w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1} | ||||
| .w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75} | ||||
| .w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)} | ||||
| .w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)} | ||||
| .w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)} | ||||
| .w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important} | ||||
| .w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important} | ||||
| .w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important} | ||||
| .w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important} | ||||
| .w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important} | ||||
| .w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important} | ||||
| .w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important} | ||||
| .w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important} | ||||
| .w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important} | ||||
| .w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important} | ||||
| .w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important} | ||||
| .w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important} | ||||
| .w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important} | ||||
| .w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important} | ||||
| .w3-padding-64{padding-top:64px!important;padding-bottom:64px!important} | ||||
| .w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important} | ||||
| .w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important} | ||||
| .w3-left{float:left!important}.w3-right{float:right!important} | ||||
| .w3-button:hover{color:#000!important;background-color:#ccc!important} | ||||
| .w3-transparent,.w3-hover-none:hover{background-color:transparent!important} | ||||
| .w3-hover-none:hover{box-shadow:none!important} | ||||
| /* Colors */ | ||||
| .w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important} | ||||
| .w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important} | ||||
| .w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important} | ||||
| .w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important} | ||||
| .w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important} | ||||
| .w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important} | ||||
| .w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important} | ||||
| .w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important} | ||||
| .w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important} | ||||
| .w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important} | ||||
| .w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important} | ||||
| .w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important} | ||||
| .w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important} | ||||
| .w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important} | ||||
| .w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important} | ||||
| .w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important} | ||||
| .w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important} | ||||
| .w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important} | ||||
| .w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important} | ||||
| .w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important} | ||||
| .w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important} | ||||
| .w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important} | ||||
| .w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important} | ||||
| .w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important} | ||||
| .w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important} | ||||
| .w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important} | ||||
| .w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important} | ||||
| .w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important} | ||||
| .w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important} | ||||
| .w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important} | ||||
| .w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important} | ||||
| .w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important} | ||||
| .w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important} | ||||
| .w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important} | ||||
| .w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important} | ||||
| .w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important} | ||||
| .w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important} | ||||
| .w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important} | ||||
| .w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important} | ||||
| .w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important} | ||||
| .w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important} | ||||
| .w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important} | ||||
| .w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important} | ||||
| .w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important} | ||||
| .w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important} | ||||
| .w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important} | ||||
| .w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important} | ||||
| .w3-text-red,.w3-hover-text-red:hover{color:#f44336!important} | ||||
| .w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important} | ||||
| .w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important} | ||||
| .w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important} | ||||
| .w3-text-white,.w3-hover-text-white:hover{color:#fff!important} | ||||
| .w3-text-black,.w3-hover-text-black:hover{color:#000!important} | ||||
| .w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important} | ||||
| .w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important} | ||||
| .w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important} | ||||
| .w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important} | ||||
| .w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important} | ||||
| .w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important} | ||||
| .w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important} | ||||
| .w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important} | ||||
| .w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important} | ||||
| .w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important} | ||||
| .w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important} | ||||
| .w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important} | ||||
| .w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important} | ||||
| .w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important} | ||||
| .w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important} | ||||
| .w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important} | ||||
| .w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important} | ||||
| .w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important} | ||||
| .w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important} | ||||
| .w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important} | ||||
| .w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important} | ||||
| .w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important} | ||||
| .w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important} | ||||
| .w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important} | ||||
| .w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important} | ||||
| .w3-border-black,.w3-hover-border-black:hover{border-color:#000!important} | ||||
| .w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important} | ||||
| .w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important} | ||||
| .w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important} | ||||
| .w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important} | ||||
| .w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important} | ||||
							
								
								
									
										5
									
								
								apps/api.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/api.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "📜", | ||||
| 	"previous": "&miGORZ8BwjHg2YO0t4bms6SI28XWPYqnqOZ8u9zsbZc=.sha256" | ||||
| } | ||||
							
								
								
									
										77
									
								
								apps/api/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								apps/api/app.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										356
									
								
								apps/api/docs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										356
									
								
								apps/api/docs.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,356 @@ | ||||
| export const docs = {}; | ||||
|  | ||||
| docs.global = `# Tilde Friends API Documentation | ||||
|  | ||||
| Welcome to the Tilde Friends API documentation. | ||||
|  | ||||
|  * [App Globals](#App_Globals) | ||||
|  * [Database Interface](#Database) | ||||
|  * [Remote Procedure Calls](#tfrpc) | ||||
|  | ||||
| <a id="App_Globals"></a> | ||||
| ## <span style="color: #aaf">App Globals</span> | ||||
| The following are functions and values exposed to all apps in their \`app.js\` or \`handler.js\`.  Most | ||||
| of these are asynchronous, returning a \`Promise\` that will be resolved when the call completes, unless | ||||
| noted otherwise. | ||||
|  | ||||
| This is all a work in progess. These are liable to change without warning.  Feedback is welcome. | ||||
|  | ||||
| The exposed functions in this API balance multiple competing needs: | ||||
|  * The surface area of the exposed API ought to be fairly minimal.  If something can be implemented entirely app-side, that is | ||||
|    generally preferred over building it into the core. | ||||
|  * Everything is built on this API.  Ideally the admin app, the SSB app, and the editor all use standard API exposed to all | ||||
|    apps, with appropriate permission guards in place making it so that only trusted apps do potentially destructive operations. | ||||
|    There will be some things here that aren't necessarily general use to support what's required. | ||||
|  | ||||
| If you are looking at the [Tilde Friends source code](https://www.tildefriends.net/~cory/releases/), | ||||
| the vast majority of these are implemented in \`src/*.js.c\` files, and exposed to apps via \`core/core.js\`. | ||||
| `; | ||||
|  | ||||
| docs['core.user.credentials.session.name'] = ` | ||||
| *String* The name of the authenticated user. | ||||
| `; | ||||
|  | ||||
| docs['app.setDocument()'] = ` | ||||
| Set the contents of the client <iframe/>. | ||||
| ### Parameters | ||||
|  * *String* **html** The HTML contents. | ||||
| `; | ||||
|  | ||||
| docs['core.apps()'] = ` | ||||
| Gets a list of apps owned by the current user. | ||||
| ### Returns | ||||
| *Array* An array of string names of the apps owned by the current user. | ||||
| `; | ||||
|  | ||||
| docs['core.url'] = ` | ||||
| The url by which the running app is being invoked. | ||||
| `; | ||||
|  | ||||
| docs['app.localStorageSet()'] = ` | ||||
| Set a value in browser local storage. | ||||
| ### Parameters | ||||
| *String* **key** The localStorage key to set. | ||||
| *String* **value** The localStorage value to set. | ||||
| `; | ||||
|  | ||||
| docs['app.localStorageGet()'] = ` | ||||
| Gets a value from browser local storage. | ||||
| ### Parameters | ||||
| *String* **key** The key with which the value was set. | ||||
| ### Returns | ||||
| *String* The value, or undefined. | ||||
| `; | ||||
|  | ||||
| docs['app.print()'] = ` | ||||
| Log information for debugging purposes to the server and to the connected browser console. | ||||
| ### Parameters | ||||
|  * ... Any args to print. | ||||
| `; | ||||
|  | ||||
| docs['ssb.createIdentity()'] = ` | ||||
| Create a new SSB identity. | ||||
| ### Returns | ||||
| *String* The created identity public key. | ||||
| `; | ||||
|  | ||||
| docs['ssb.getIdentities()'] = ` | ||||
| Get all SSB identities owned by the current user. | ||||
| ### Returns | ||||
| *Array* An array of public key strings. | ||||
| `; | ||||
|  | ||||
| docs['ssb.sqlAsync()'] = ` | ||||
| Run an SQL query against the sqlite database. | ||||
| ### Parameters | ||||
|  * *String* **query** The sqlite query. | ||||
|  * *Array* **args** The query arguments to bind. | ||||
|  * *Function* **callback** Callback called for each row result. | ||||
| `; | ||||
|  | ||||
| docs['ssb.appendMessageWithIdentity()'] = ` | ||||
| Signs and stores a message in the SSB database. | ||||
| ### Parameters | ||||
|  * *String* **id** The public key of an SSB identity owned by the authenticated user. | ||||
|  * *Object* **message** The unsigned message. | ||||
| `; | ||||
|  | ||||
| docs['ssb.storeMessage()'] = ` | ||||
| Verifies and stores a signed message in the SSB database. | ||||
| ### Parameters | ||||
|  * *Object* **message** The valid, signed message to store. | ||||
| `; | ||||
|  | ||||
| docs['ssb.blobStore()'] = ` | ||||
| Store a blob in the SSB database. | ||||
| ### Parameters | ||||
|  * *String*/*Uint8Array* **blob** The blob contents to store | ||||
| ### Returns | ||||
| *String* The stored blob ID. | ||||
| `; | ||||
|  | ||||
| docs['ssb.blobGet()'] = ` | ||||
| Fetches a blob from the database. | ||||
| ### Parameters | ||||
|  * *String* **blob_id** The blob identifier to fetch (\`&....sha256\`). | ||||
| ### Returns | ||||
| *ArrayBuffer* The blob data. | ||||
| `; | ||||
|  | ||||
| docs['print()'] = ` | ||||
| Log debug information both to the server's console and to the visiting user's browser console when possible. | ||||
| ### Parameters | ||||
|  * **...** Whatever you want to log.  Will be joined with spaces. | ||||
| `; | ||||
|  | ||||
| docs['database()'] = ` | ||||
| Returns a database instance that is specific to the authenticated user and the given key. | ||||
| ### Parameters | ||||
|  * *String* **key** The database key. | ||||
| ### Returns | ||||
|  *Database* A database. | ||||
| `; | ||||
|  | ||||
| docs['my_shared_database()'] = ` | ||||
| Returns a database instance that is specific to the authenticated user and the given key. | ||||
| ### Parameters | ||||
|  * *String* **package_name** The database package name. | ||||
|  * *String* **key** The database key. | ||||
| ### Returns | ||||
| *Database* A database. | ||||
| `; | ||||
|  | ||||
| docs['shared_database()'] = ` | ||||
| Returns a database instance that is shared between all users of the app, determined by its owner and app name. | ||||
| ### Parameters | ||||
|  * *String* **key** The database key. | ||||
| ### Returns | ||||
| *Database* A database. | ||||
| `; | ||||
|  | ||||
| docs['base64Decode()'] = ` | ||||
| Decode a base64 string to bytes. | ||||
|  | ||||
| Completes synchronously. | ||||
| ### Parameters | ||||
|  * *String* value The base64-encoded string. | ||||
| ### Returns | ||||
| *Uint8Array* The decoded bytes. | ||||
| `; | ||||
|  | ||||
| docs['base64Encode()'] = ` | ||||
| Encode bytes to a base64 string. | ||||
|  | ||||
| Completes synchronously. | ||||
| ### Parameters | ||||
|  * *Uint8Array* The bytes to encode. | ||||
| ### Returns | ||||
| *String* The base64-encoded string. | ||||
| `; | ||||
|  | ||||
| docs['utf8Decode()'] = ` | ||||
| Decode UTF-8 bytes to a string. | ||||
|  | ||||
| Completes synchronously. | ||||
| ### Parameters | ||||
|  * *Uint8Array* **value** The value to decode. | ||||
| ### Returns | ||||
| *String* The value as a string. | ||||
| `; | ||||
|  | ||||
| docs['utf8Encode()'] = ` | ||||
| Encodes a string to UTF-8 bytes. | ||||
|  | ||||
| Completes synchronously. | ||||
| ### Parameters | ||||
|  * *String* **value** The value to encode. | ||||
| ### Returns | ||||
| *Uint8Array* The encoded \`value\`. | ||||
| `; | ||||
|  | ||||
| docs['setTimeout()'] = ` | ||||
| Call a function after some delay. | ||||
| ### Parameters | ||||
|  * *Function* **callback** The function to call. | ||||
|  * *Number* **timeout** Number of milliseconds to wait before calling the callback function. | ||||
| `; | ||||
|  | ||||
| docs['parseHttpRequest()'] = ` | ||||
| Parses an HTTP request. | ||||
| ### Parameters | ||||
|  * *Uint8Array* **request** The request data.  Maybe be partial or contain extra data.  The return value will | ||||
|     indicate when and where it is complete. | ||||
|  * *Number* **last_length** The length of the data passed on a previous attempt for the same request, or 0 initially. | ||||
| ### Returns | ||||
|  * *Integer* **-2** if the request is incomplete. | ||||
|  * *Integer* **-1** if the request could not be parsed. | ||||
|  * *Object* An object with **bytes_parsed**, **minor_version**, **path**, and **headers** fields on successful parse. | ||||
| `; | ||||
|  | ||||
| docs['parseHttpResponse()'] = ` | ||||
| Parses an HTTP response. | ||||
| ### Parameters | ||||
|  * *Uint8Array* **response** The response data.  Maybe be partial or contain extra data.  The return value will | ||||
|     indicate when and where it is complete. | ||||
|  * *Number* **last_length** The length of the data passed on a previous attempt for the same response, or 0 initially. | ||||
| ### Returns | ||||
|  * *Integer* **-2** if the response is incomplete. | ||||
|  * *Integer* **-1** if the response could not be parsed. | ||||
|  * *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse. | ||||
| `; | ||||
|  | ||||
| docs['sha1Digest()'] = ` | ||||
| Calculates a SHA1 digest. | ||||
|  | ||||
| Completes synchronously. | ||||
| ### Parameters | ||||
|  * *String* **value** The value for which to calculate the digest. | ||||
| ### Returns | ||||
| *String* The SHA1 digest of UTF-8 encoded \`value\`. | ||||
| `; | ||||
|  | ||||
| docs['maskBytes()'] = ` | ||||
| Masks bytes for WebSocket communication. | ||||
|  | ||||
| Completes synchronously. | ||||
| ### Parameters | ||||
|  * *Uint8Array* **bytes** The byte array of data to mask. | ||||
|  * *Uint32* **mask** The mask to apply. | ||||
| ### Returns | ||||
| *Uint32Array* The masked bytes. | ||||
| `; | ||||
|  | ||||
| docs['exit()'] = ` | ||||
| Exits the app.  But why would you want to do that? | ||||
|  | ||||
| Completes synchronously. | ||||
| ### Parameters | ||||
|  * *Integer* **exit_code** System exit code. | ||||
| `; | ||||
|  | ||||
| docs['version()'] = ` | ||||
| Gets version information for the running server. | ||||
| ### Returns | ||||
| *Object* Keys are things like \`name\` and \`number\` for the server itself and \`libuv\` and \`openssl\` for | ||||
| dependencies.  Values are *String* version numbers. | ||||
| `; | ||||
|  | ||||
| docs['platform()'] = ` | ||||
| Gets the host operating system platform of the running server. | ||||
| ### Returns | ||||
| *String* The platform, one of \`windows\`, \`android\`, \`linux\`, or \`other\`. | ||||
| `; | ||||
|  | ||||
| docs['getFile()'] = ` | ||||
| Gets a file from the running app. | ||||
| ### Parameters | ||||
|  * *String* **name** Name of the file to retrieve. | ||||
| ### Returns | ||||
| *Uint8Array* The contents of a file from the app with the given name, or *undefined*. | ||||
| `; | ||||
|  | ||||
| docs.database = ` | ||||
| # <span style="color: #aaf">Database</span> | ||||
| Local-only storage is provided by a \`Database\` type representing a key-value store. | ||||
| `; | ||||
|  | ||||
| docs['database.get()'] = ` | ||||
| Gets a value from the database. | ||||
| ### Parameters | ||||
|  * *String* **key** The key. | ||||
| ### Returns | ||||
| *String* The value from the database or undefined if not found. | ||||
| `; | ||||
|  | ||||
| docs['database.getAll()'] = ` | ||||
| Gets all keys from the database. | ||||
| ### Returns | ||||
| *Array* An array of *String* key names for all keys in the given database. | ||||
| `; | ||||
|  | ||||
| docs['database.getLike()'] = ` | ||||
| Gets all keys and values from the database matching a pattern. | ||||
| ### Parameters | ||||
|  * *String* **pattern** An sqlite \`LIKE\` pattern to match keys against. | ||||
| ### Returns | ||||
| *Object* An object whose keys are the database keys and values are the database values that match the given pattern. | ||||
| `; | ||||
|  | ||||
| docs['database.set()'] = ` | ||||
| Sets a value in the database, creating a new entry or replacing an existing entry. | ||||
| ### Parameters | ||||
|  * *String* **key** The key. | ||||
|  * *String* **value** The value. | ||||
| `; | ||||
|  | ||||
| docs['database.exchange()'] = ` | ||||
| Performs an atomic compare and exchange operation, setting a value in the database only if its current value matches what is expected. | ||||
| ### Parameters | ||||
|  * *String* **key** The key. | ||||
|  * *String* **expected** The expected value. | ||||
|  * *String* **value** The new value. | ||||
|  ### Returns | ||||
|  *Boolean* true if the value is now the given value. | ||||
| `; | ||||
|  | ||||
| docs['database.remove()'] = ` | ||||
| Removes an entry from the database if it exists. | ||||
| ### Parameters | ||||
|  * *String* **key** The key. | ||||
| `; | ||||
|  | ||||
| docs.tfrpc = ` | ||||
| # <span style="color: #aaf" id="tfrpc">tfrpc</span> | ||||
| \`tfrpc.js\` is a small helper script that is available to be used to facilitate communication between parts of an application. | ||||
|  | ||||
| \`tfrpc.js\` can be used to asynchronously make calls between the app code running in a sandboxed iframe in the browser | ||||
| and the app process on the server. | ||||
|  | ||||
| From \`app.js\`: | ||||
| \`\`\` | ||||
| import * as tfrpc from '/tfrpc.js'; | ||||
| \`\`\` | ||||
|  | ||||
|  | ||||
| From script running in the browser: | ||||
| \`\`\` | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
| \`\`\` | ||||
|  | ||||
| Either side can register or call functions, though they must be registered before they can be called.  Arguments and return | ||||
| values are ultimately serialized by means that attempt to preserve most JSON-serializable values as well as functions themselves. | ||||
| `; | ||||
|  | ||||
| docs['tfrpc.register()'] = ` | ||||
| Register a function, allowing it to be called remotely. | ||||
| ### Parameters | ||||
|  * *Function* **function** The function to register.  Its name will be how it will be called. | ||||
| `; | ||||
|  | ||||
| docs['tfrpc.rpc.*()'] = ` | ||||
| Call a remote function. | ||||
| ### Parameters | ||||
|  * **...** Parameters to pass to the function. | ||||
| ### Returns | ||||
| The return value of the called function. | ||||
| `; | ||||
							
								
								
									
										5
									
								
								apps/apps.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/apps.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "💻", | ||||
| 	"previous": "&icsPplXHgmpkbNWyo/WTvRdT/A8BXxW4lJixOtP4ueQ=.sha256" | ||||
| } | ||||
							
								
								
									
										182
									
								
								apps/apps/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								apps/apps/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| /** | ||||
|  * Fetches information about the applications | ||||
|  * @param apps Record<appName, blobId> | ||||
|  * @returns an object including the apps' name, emoji, and blobs ids | ||||
|  */ | ||||
| async function fetch_info(apps) { | ||||
| 	let result = {}; | ||||
|  | ||||
| 	// For each app | ||||
| 	for (let [key, value] of Object.entries(apps)) { | ||||
| 		// Get it's blob and parse it | ||||
| 		let blob = await ssb.blobGet(value); | ||||
| 		blob = blob ? utf8Decode(blob) : '{}'; | ||||
|  | ||||
| 		// Add it to the result object | ||||
| 		result[key] = JSON.parse(blob); | ||||
| 	} | ||||
|  | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * | ||||
|  * | ||||
|  */ | ||||
| async function fetch_shared_apps() { | ||||
| 	let messages = {}; | ||||
|  | ||||
| 	await ssb.sqlAsync( | ||||
| 		` | ||||
| 				SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature | ||||
| 				FROM messages_fts('"application/tildefriends"') | ||||
| 				JOIN messages ON messages.rowid = messages_fts.rowid | ||||
| 				ORDER BY messages.timestamp | ||||
| 		`, | ||||
| 		[], | ||||
| 		function (row) { | ||||
| 			let content = JSON.parse(row.content); | ||||
| 			for (let mention of content.mentions) { | ||||
| 				if (mention?.type === 'application/tildefriends') { | ||||
| 					messages[JSON.stringify([row.author, mention.name])] = { | ||||
| 						message: row, | ||||
| 						blob: mention.link, | ||||
| 						name: mention.name, | ||||
| 					}; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	); | ||||
|  | ||||
| 	let result = {}; | ||||
| 	for (let app of Object.values(messages).sort( | ||||
| 		(x, y) => y.message.timestamp - x.message.timestamp | ||||
| 	)) { | ||||
| 		let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app.blob))); | ||||
| 		if (app_object) { | ||||
| 			app_object.blob_id = app.blob; | ||||
| 			result[app.name] = app_object; | ||||
| 		} | ||||
| 	} | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| async function main() { | ||||
| 	const apps = await fetch_info(await core.apps()); | ||||
| 	const core_apps = await fetch_info(await core.apps('core')); | ||||
| 	const shared_apps = await fetch_shared_apps(); | ||||
|  | ||||
| 	const stylesheet = ` | ||||
| 		body { | ||||
| 			color: whitesmoke; | ||||
| 			font-family: sans-serif; | ||||
| 			margin: 16px; | ||||
| 		} | ||||
| 		.container { | ||||
| 			display: grid; | ||||
| 			grid-template-columns: repeat(auto-fill, 64px); | ||||
| 			gap: 1em; | ||||
| 			justify-content: space-around; | ||||
| 			background-color: #ffffff10; | ||||
| 			border: 2px solid #073642; | ||||
| 			border-radius: 8px; | ||||
| 		} | ||||
|  | ||||
| 		.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; | ||||
| 			color: whitesmoke; | ||||
| 		} | ||||
| 	`; | ||||
|  | ||||
| 	const body = ` | ||||
| 		<h1 style="text-shadow: #808080 0 0 10px;">Welcome to Tilde Friends.</h1> | ||||
|  | ||||
| 		<h2>your apps</h2> | ||||
| 		<div id="apps" class="container"></div> | ||||
|  | ||||
| 		<h2>shared apps</h2> | ||||
| 		<div id="shared_apps" class="container"></div> | ||||
|  | ||||
| 		<h2>core apps</h2> | ||||
| 		<div id="core_apps" class="container"></div> | ||||
| 	`; | ||||
|  | ||||
| 	const script = ` | ||||
| 		/* | ||||
| 		 * Creates a list of apps | ||||
| 		 * @param id the id of the element to populate | ||||
| 		 * @param name (a username, 'core' or undefined) | ||||
| 		 * @param apps Object, a list of apps | ||||
| 		 */ | ||||
| 		function populate_apps(id, name, apps) { | ||||
| 			// Our target | ||||
| 			var list = document.getElementById(id); | ||||
|  | ||||
| 			// For each app in the provided list | ||||
| 			for (let app of Object.keys(apps).sort()) { | ||||
|  | ||||
| 				// Create the item | ||||
| 				let div = list.appendChild(document.createElement('div')); | ||||
| 				div.classList.add('app'); | ||||
|  | ||||
| 				// The app's icon | ||||
| 				let href = name ? '/~' + name + '/' + app + '/' : ('/' + apps[app].blob_id + '/'); | ||||
| 				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 = href; | ||||
| 				icon_a.target = '_top'; | ||||
| 				div.appendChild(icon_a); | ||||
|  | ||||
| 				// The app's name | ||||
| 				let a = document.createElement('a'); | ||||
| 				a.appendChild(document.createTextNode(app)); | ||||
| 				a.href = href; | ||||
| 				a.target = '_top'; | ||||
| 				div.appendChild(a); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)}); | ||||
| 		populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)}); | ||||
| 		populate_apps('shared_apps', undefined, ${JSON.stringify(shared_apps)}); | ||||
| 	`; | ||||
|  | ||||
| 	// Build the document | ||||
| 	const document = ` | ||||
| 	<!DOCTYPE html> | ||||
| 	<html> | ||||
| 		<head> | ||||
| 			<style> | ||||
| 				${stylesheet} | ||||
| 			</style> | ||||
| 		</head> | ||||
|  | ||||
| 		<body> | ||||
| 			${body} | ||||
| 		</body> | ||||
|  | ||||
| 		<script> | ||||
| 			${script} | ||||
| 		</script> | ||||
| 	</html>`; | ||||
|  | ||||
| 	// Send it to the browser | ||||
| 	app.setDocument(document); | ||||
| } | ||||
|  | ||||
| main(); | ||||
							
								
								
									
										5
									
								
								apps/blog.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/blog.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "🪵", | ||||
| 	"previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256" | ||||
| } | ||||
							
								
								
									
										8
									
								
								apps/blog/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								apps/blog/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import * as blog from './blog.js'; | ||||
|  | ||||
| async function main() { | ||||
| 	let blogs = await blog.get_posts(); | ||||
| 	await app.setDocument(blog.render_html(blogs)); | ||||
| } | ||||
|  | ||||
| main(); | ||||
							
								
								
									
										207
									
								
								apps/blog/blog.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								apps/blog/blog.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,207 @@ | ||||
| import * as commonmark from './commonmark.min.js'; | ||||
|  | ||||
| function escape(text) { | ||||
| 	return (text ?? '') | ||||
| 		.replaceAll('&', '&') | ||||
| 		.replaceAll('<', '<') | ||||
| 		.replaceAll('>', '>'); | ||||
| } | ||||
|  | ||||
| function escapeAttribute(text) { | ||||
| 	return (text ?? '') | ||||
| 		.replaceAll('&', '&') | ||||
| 		.replaceAll('<', '<') | ||||
| 		.replaceAll('>', '>') | ||||
| 		.replaceAll('"', '"') | ||||
| 		.replaceAll("'", '''); | ||||
| } | ||||
|  | ||||
| export async function get_blog_message(id) { | ||||
| 	let message; | ||||
| 	await ssb.sqlAsync( | ||||
| 		'SELECT author, timestamp, content FROM messages WHERE id = ?', | ||||
| 		[id], | ||||
| 		function (row) { | ||||
| 			let content = JSON.parse(row.content); | ||||
| 			message = { | ||||
| 				author: row.author, | ||||
| 				timestamp: row.timestamp, | ||||
| 				blog: content?.blog, | ||||
| 				title: content?.title, | ||||
| 			}; | ||||
| 		} | ||||
| 	); | ||||
| 	if (message) { | ||||
| 		await ssb.sqlAsync( | ||||
| 			` | ||||
| 				SELECT json_extract(content, '$.name') AS name | ||||
| 				FROM messages | ||||
| 				WHERE author = ? | ||||
| 				AND json_extract(content, '$.type') = 'about' | ||||
| 				AND json_extract(content, '$.about') = author | ||||
| 				AND name IS NOT NULL | ||||
| 				ORDER BY sequence DESC LIMIT 1 | ||||
| 			`, | ||||
| 			[message.author], | ||||
| 			function (row) { | ||||
| 				message.name = row.name; | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| 	return message; | ||||
| } | ||||
|  | ||||
| export function markdown(md) { | ||||
| 	let reader = new commonmark.Parser({safe: true}); | ||||
| 	let writer = new commonmark.HtmlRenderer(); | ||||
| 	let parsed = reader.parse(md || ''); | ||||
| 	let walker = parsed.walker(); | ||||
| 	let event, node; | ||||
| 	while ((event = walker.next())) { | ||||
| 		node = event.node; | ||||
| 		if (event.entering) { | ||||
| 			if (node.destination?.startsWith('&')) { | ||||
| 				node.destination = | ||||
| 					'/' + node.destination + '/view?filename=' + node.firstChild?.literal; | ||||
| 			} else if ( | ||||
| 				node.destination?.startsWith('@') || | ||||
| 				node.destination?.startsWith('%') | ||||
| 			) { | ||||
| 				node.destination = '/~core/ssb/#' + escape(node.destination); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return writer.render(parsed); | ||||
| } | ||||
|  | ||||
| export async function render_blog_post_html(blog_post) { | ||||
| 	let blob = utf8Decode(await ssb.blobGet(blog_post.blog)); | ||||
| 	return `<!DOCTYPE html> | ||||
| 		<html> | ||||
| 			<head> | ||||
| 				<title>🪵Tilde Friends Blog - ${markdown(blog_post.title)}</title> | ||||
| 				<base target="_top"> | ||||
| 			</head> | ||||
| 			<body> | ||||
| 				<h1><a href="./">🪵Tilde Friends Blog</a></h1> | ||||
| 				<div> | ||||
| 					<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div> | ||||
| 					<div>${markdown(blob)}</div> | ||||
| 				</div> | ||||
| 			</body> | ||||
| 		</html> | ||||
| 	`; | ||||
| } | ||||
|  | ||||
| function render_blog_post(blog_post) { | ||||
| 	return ` | ||||
| 		<div> | ||||
| 			<h2><a href="/~${core.app.owner}/${core.app.name}/${escapeAttribute(blog_post.id)}">${escape(blog_post.title)}</a></h2> | ||||
| 			<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div> | ||||
| 			<div>${markdown(blog_post.summary)}</div> | ||||
| 		</div> | ||||
| 	`; | ||||
| } | ||||
|  | ||||
| export function render_html(blogs) { | ||||
| 	return `<!DOCTYPE html> | ||||
| 		<html> | ||||
| 			<head> | ||||
| 				<title>🪵Tilde Friends Blog</title> | ||||
| 				<link href="./atom" type="application/atom+xml" rel="alternate" title="🪵Tilde Blog"/> | ||||
| 				<style> | ||||
| 					html { | ||||
| 						background-color: #ccc; | ||||
| 					} | ||||
| 				</style> | ||||
| 				<base target="_top"> | ||||
| 			</head> | ||||
| 			<body> | ||||
| 				<div style="display: flex; flex-direction: row; align-items: center; gap: 1em"> | ||||
| 					<h1>🪵Tilde Friends Blog</h1> | ||||
| 					<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div> | ||||
| 				</div> | ||||
| 				${blogs.map((blog_post) => render_blog_post(blog_post)).join('\n')} | ||||
| 			</body> | ||||
| 		</html>`; | ||||
| } | ||||
|  | ||||
| function render_blog_post_atom(blog_post) { | ||||
| 	return `<entry> | ||||
| 		<title>${escape(blog_post.title)}</title> | ||||
| 		<link href="/~cory/ssb/#${blog_post.id}" /> | ||||
| 		<id>${blog_post.id}</id> | ||||
| 		<published>${escape(new Date(blog_post.timestamp).toString())}</published> | ||||
| 		<summary>${escape(blog_post.summary)}</summary> | ||||
| 		<author> | ||||
| 			<name>${escape(blog_post.name)}</name> | ||||
| 			<feed>${escape(blog_post.author)}</feed> | ||||
| 		</author> | ||||
| 	</entry>`; | ||||
| } | ||||
|  | ||||
| export function render_atom(blogs) { | ||||
| 	return `<?xml version="1.0" encoding="utf-8"?> | ||||
| <feed xmlns="http://www.w3.org/2005/Atom"> | ||||
| 	<title>🪵Tilde Blog</title> | ||||
| 	<subtitle>A subtitle.</subtitle> | ||||
| 	<link href="${core.url}/atom" rel="self"/> | ||||
| 	<link href="${core.url}"/> | ||||
| 	<id>${core.url}</id> | ||||
| 	<updated>${new Date().toString()}</updated> | ||||
| 	${blogs.map((blog_post) => render_blog_post_atom(blog_post)).join('\n')} | ||||
| </feed>`; | ||||
| } | ||||
|  | ||||
| export async function get_posts() { | ||||
| 	let blogs = []; | ||||
| 	let ids = await ssb.getIdentities(); | ||||
| 	await ssb.sqlAsync( | ||||
| 		` | ||||
| 		WITH | ||||
| 			blogs AS ( | ||||
| 				SELECT | ||||
| 					messages.author, | ||||
| 					messages.id, | ||||
| 					json_extract(messages.content, '$.title') AS title, | ||||
| 					json_extract(messages.content, '$.summary') AS summary, | ||||
| 					json_extract(messages.content, '$.blog') AS blog, | ||||
| 					messages.timestamp | ||||
| 				FROM messages_fts('blog') | ||||
| 				JOIN messages ON messages.rowid = messages_fts.rowid | ||||
| 				WHERE json_extract(messages.content, '$.type') = 'blog'), | ||||
| 			public AS ( | ||||
| 				SELECT author FROM ( | ||||
| 					SELECT | ||||
| 						messages.author, | ||||
| 						RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank, | ||||
| 						json_extract(messages.content, '$.publicWebHosting') AS is_public | ||||
| 					FROM messages_fts('about') | ||||
| 					JOIN messages ON messages.rowid = messages_fts.rowid | ||||
| 					WHERE json_extract(messages.content, '$.type') = 'about' AND is_public IS NOT NULL) | ||||
| 				WHERE author_rank = 1 AND is_public), | ||||
| 			names AS ( | ||||
| 				SELECT author, name FROM ( | ||||
| 					SELECT | ||||
| 						messages.author, | ||||
| 						RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank, | ||||
| 						json_extract(messages.content, '$.name') AS name | ||||
| 					FROM messages_fts('about') | ||||
| 					JOIN messages ON messages.rowid = messages_fts.rowid | ||||
| 					WHERE json_extract(messages.content, '$.type') = 'about' AND | ||||
| 						json_extract(messages.content, '$.about') = messages.author AND | ||||
| 						name IS NOT NULL) | ||||
| 				WHERE author_rank = 1) | ||||
| 		SELECT blogs.*, names.name FROM blogs | ||||
| 		JOIN json_each(?) AS self ON self.value = blogs.author | ||||
| 		JOIN public ON public.author = blogs.author | ||||
| 		LEFT OUTER JOIN names ON names.author = blogs.author | ||||
| 		ORDER BY blogs.timestamp DESC LIMIT 20 | ||||
| 	`, | ||||
| 		[JSON.stringify(ids)], | ||||
| 		function (row) { | ||||
| 			blogs.push(row); | ||||
| 		} | ||||
| 	); | ||||
| 	return blogs; | ||||
| } | ||||
							
								
								
									
										1
									
								
								apps/blog/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/blog/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										51
									
								
								apps/blog/handler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								apps/blog/handler.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| import * as blog from './blog.js'; | ||||
|  | ||||
| async function main() { | ||||
| 	if (request.path.startsWith('%') && request.path.endsWith('.sha256')) { | ||||
| 		let id = request.path.startsWith('%25') | ||||
| 			? '%' + request.path.substring(3) | ||||
| 			: request.path; | ||||
| 		let message = await blog.get_blog_message(id); | ||||
| 		if (message) { | ||||
| 			respond({ | ||||
| 				data: await blog.render_blog_post_html(message), | ||||
| 				content_type: 'text/html; charset=utf-8', | ||||
| 			}); | ||||
| 		} else { | ||||
| 			respond({ | ||||
| 				data: `Message ${id} not found.`, | ||||
| 				content_type: 'text/html; charset=utf-8', | ||||
| 			}); | ||||
| 		} | ||||
| 	} else if (request.path == 'atom') { | ||||
| 		let blogs = await blog.get_posts(); | ||||
| 		respond({ | ||||
| 			data: blog.render_atom(blogs), | ||||
| 			content_type: 'application/atom+xml', | ||||
| 		}); | ||||
| 	} else { | ||||
| 		let blogs = await blog.get_posts(); | ||||
| 		for (let blog_post of blogs) { | ||||
| 			let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase(); | ||||
| 			if (request.path === title) { | ||||
| 				respond({ | ||||
| 					data: await blog.render_blog_post_html(blog_post), | ||||
| 					content_type: 'text/html; charset=utf-8', | ||||
| 				}); | ||||
| 				return; | ||||
| 			} | ||||
| 		} | ||||
| 		respond({ | ||||
| 			data: blog.render_html(blogs), | ||||
| 			content_type: 'text/html; charset=utf-8', | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| main().catch(function (error) { | ||||
| 	respond({ | ||||
| 		data: `<!DOCTYPE html> | ||||
| 	<pre style="color: #f00">${error.message}\n${error.stack}</pre>`, | ||||
| 		content_type: 'text/html', | ||||
| 	}); | ||||
| }); | ||||
							
								
								
									
										120
									
								
								apps/blog/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								apps/blog/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								apps/blog/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/blog/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -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,16 +0,0 @@ | ||||
| # Tilde Friends Developer's Guide | ||||
| [Back to index](#index) | ||||
|  | ||||
| A Tilde Friends application runs on the server.  To make an interesting | ||||
| application that interacts with the client, it's necessary to understand | ||||
| how the parts work together. | ||||
|  | ||||
| ## Hello, world! | ||||
|  | ||||
| A simple starting point.  Presents `Hello, world!` in the browser when | ||||
| visited. | ||||
|  | ||||
| **app.js**: | ||||
| ``` | ||||
| app.setDocument('<h1>Hello, world!</h1>'); | ||||
| ``` | ||||
| @@ -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,67 +0,0 @@ | ||||
| # Tilde Friends Structure | ||||
| [Back to index](#index) | ||||
|  | ||||
| Tilde Friends is a mostly-self-contained executable written in C. | ||||
|  | ||||
| In combines the following key components: | ||||
| - A Secure Scuttlebutt (SSB) client/server.  This talks with other SSB | ||||
|   instances, storing messages and blobs for anyone visible to local | ||||
|   users as they are encountered and sharing anything published locally | ||||
|   as appropriate. | ||||
| - An sqlite database.  This is where the SSB instance stores its data. | ||||
|   The general schema involves a `messages` table, storing mostly JSON, | ||||
|   a `blobs` table storing arbitrary blob data, and a `properties` table, | ||||
|   storing arbitrary state gleaned from `messages` and `blobs`, generally | ||||
|   updated on demand and incrementally. | ||||
| - A QuickJS runtime.  The core process runs stock scripts and has access | ||||
|   and permission to use all resources.  All other processes, which | ||||
|   includes everything which runs untrusted code created by Tilde Friends | ||||
|   users, are strictly sandboxed in ways similar to how web browsers run | ||||
|   untrusted code.  All attempts to access potentially sensitive resources | ||||
|   are mediated through the core process. | ||||
|  | ||||
| When run with no arguments, it starts a web server on | ||||
| [http://localhost:12345/](http://localhost:12345/) and an SSB server. | ||||
|  | ||||
| ## Web Interface | ||||
| The Tilde Friends web server provides access to Tilde Friends applications, | ||||
| which are arbitrary user-defined web applications. | ||||
|  | ||||
| At the top left, in addition to some basic navigation links, is an `edit` | ||||
| link.  Anyone can view, modify, and run in-place the code to any Tilde | ||||
| Friends application by using the in-browser editor. | ||||
|  | ||||
| At the top right, one can `login` (to save work in their own space) | ||||
| or `logout` (proceeding as a guest). | ||||
|  | ||||
| The rest of the page is an iframe belonging to the application. | ||||
|  | ||||
| ## Special Paths | ||||
|  | ||||
| - `/~user/app/` - Tilde Friends application paths take the form `/~user/app/`, where `user` | ||||
| is a username of a Tilde Friends account, and `app` is an arbitrary name | ||||
| of an application saved by the given user. | ||||
| - `/~user/app/file` - A raw file in an app. | ||||
| - `/&blobid.ed25519` - A raw blob.  Content-Type is inferred for at least | ||||
| 	a few common image types. | ||||
|  | ||||
| ## Communication Channels | ||||
| Web Browser <-> Core <-> Sandbox | ||||
|  | ||||
| Visiting an application path delivers stock HTML and JavaScript which | ||||
| establishes a WebSocket connection back to the server. | ||||
|  | ||||
| At this point, a new sandbox process is started in Tilde Friends, much | ||||
| as a new sandboxed process might be started for a new tab in a web | ||||
| browser.  This process has a custom RPC connection to the core process | ||||
| which holds the WebSocket connection to the browser. | ||||
|  | ||||
| The custom RPC communication between the sandbox process and the core | ||||
| process facilitates calling functions asynchronously.  Calling a remote | ||||
| function (ie. a function in another process) returns a `Promise`.  In | ||||
| 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 | ||||
| populate the app's iframe in the web browser with its own client web | ||||
| application resources. | ||||
| @@ -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/follow.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								apps/follow.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "➡️" | ||||
| } | ||||
							
								
								
									
										317
									
								
								apps/follow/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								apps/follow/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,317 @@ | ||||
| let g_about_cache = {}; | ||||
|  | ||||
| async function query(sql, args) { | ||||
| 	let result = []; | ||||
| 	await ssb.sqlAsync(sql, args, function (row) { | ||||
| 		result.push(row); | ||||
| 	}); | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| async function 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 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 function contact(id, last_row_id, following, max_row_id) { | ||||
| 	return await contacts_internal(id, last_row_id, following, max_row_id); | ||||
| } | ||||
|  | ||||
| async function following_deep_internal( | ||||
| 	ids, | ||||
| 	depth, | ||||
| 	blocking, | ||||
| 	last_row_id, | ||||
| 	following, | ||||
| 	max_row_id | ||||
| ) { | ||||
| 	let contacts = await Promise.all( | ||||
| 		[...new Set(ids)].map((x) => 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 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 function following_deep(ids, depth, blocking) { | ||||
| 	let db = await database('cache'); | ||||
| 	const k_cache_version = 5; | ||||
| 	let cache = await db.get('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 query( | ||||
| 			` | ||||
| 			SELECT MAX(rowid) AS max_row_id FROM messages | ||||
| 		`, | ||||
| 			[] | ||||
| 		) | ||||
| 	)[0].max_row_id; | ||||
| 	let result = await 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); | ||||
| 	await db.set('following', store); | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| async function fetch_about(db, ids, users) { | ||||
| 	const k_cache_version = 1; | ||||
| 	let cache = await db.get('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 = 0; | ||||
| 	await ssb.sqlAsync( | ||||
| 		` | ||||
| 			SELECT MAX(rowid) AS max_row_id FROM messages | ||||
| 		`, | ||||
| 		[], | ||||
| 		function (row) { | ||||
| 			max_row_id = row.max_row_id; | ||||
| 		} | ||||
| 	); | ||||
| 	for (let id of Object.keys(cache.about)) { | ||||
| 		if (ids.indexOf(id) == -1) { | ||||
| 			delete cache.about[id]; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	let abouts = []; | ||||
| 	await ssb.sqlAsync( | ||||
| 		` | ||||
| 				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 db.set('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 function getAbout(db, id) { | ||||
| 	if (g_about_cache[id]) { | ||||
| 		return g_about_cache[id]; | ||||
| 	} | ||||
| 	let o = await db.get(id + ':about'); | ||||
| 	const k_version = 4; | ||||
| 	let f = o ? JSON.parse(o) : o; | ||||
| 	if (!f || f.version != k_version) { | ||||
| 		f = {about: {}, sequence: 0, version: k_version}; | ||||
| 	} | ||||
| 	await ssb.sqlAsync( | ||||
| 		'SELECT ' + | ||||
| 			'  sequence, ' + | ||||
| 			'  content ' + | ||||
| 			'FROM messages ' + | ||||
| 			'WHERE ' + | ||||
| 			'  author = ?1 AND ' + | ||||
| 			'  sequence > ?2 AND ' + | ||||
| 			"  json_extract(content, '$.type') = 'about' AND " + | ||||
| 			"  json_extract(content, '$.about') = ?1 " + | ||||
| 			'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' + | ||||
| 			'ORDER BY sequence', | ||||
| 		[id, f.sequence], | ||||
| 		function (row) { | ||||
| 			f.sequence = row.sequence; | ||||
| 			if (row.content) { | ||||
| 				let about = {}; | ||||
| 				try { | ||||
| 					about = JSON.parse(row.content); | ||||
| 				} catch {} | ||||
| 				delete about.about; | ||||
| 				delete about.type; | ||||
| 				f.about = Object.assign(f.about, about); | ||||
| 			} | ||||
| 		} | ||||
| 	); | ||||
| 	let j = JSON.stringify(f); | ||||
| 	if (o != j) { | ||||
| 		await db.set(id + ':about', j); | ||||
| 	} | ||||
| 	g_about_cache[id] = f.about; | ||||
| 	return f.about; | ||||
| } | ||||
|  | ||||
| async function getSize(db, id) { | ||||
| 	let size = 0; | ||||
| 	await ssb.sqlAsync( | ||||
| 		'SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1', | ||||
| 		[id], | ||||
| 		function (row) { | ||||
| 			size += row.size; | ||||
| 		} | ||||
| 	); | ||||
| 	return size; | ||||
| } | ||||
|  | ||||
| async function getSizes(ids) { | ||||
| 	let sizes = {}; | ||||
| 	await ssb.sqlAsync( | ||||
| 		` | ||||
| 			SELECT | ||||
| 				author, | ||||
| 				(SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(messages.id))) AS size | ||||
| 			FROM messages | ||||
| 			JOIN json_each(?) AS ids ON author = ids.value | ||||
| 			GROUP BY author | ||||
| 		`, | ||||
| 		[JSON.stringify(ids)], | ||||
| 		function (row) { | ||||
| 			sizes[row.author] = row.size; | ||||
| 		} | ||||
| 	); | ||||
| 	return sizes; | ||||
| } | ||||
|  | ||||
| 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; | ||||
| } | ||||
|  | ||||
| function escape(value) { | ||||
| 	return value | ||||
| 		.replaceAll('&', '&') | ||||
| 		.replaceAll('<', '<') | ||||
| 		.replaceAll('>', '>'); | ||||
| } | ||||
|  | ||||
| async function main() { | ||||
| 	await app.setDocument('<pre style="color: #fff">building...</pre>'); | ||||
| 	let db = await database('ssb'); | ||||
| 	let whoami = await ssb.getIdentities(); | ||||
| 	let tree = ''; | ||||
| 	await app.setDocument( | ||||
| 		`<pre style="color: #fff">Enumerating followed users...</pre>` | ||||
| 	); | ||||
| 	let following = await following_deep(whoami, 2, {}); | ||||
| 	await app.setDocument( | ||||
| 		`<pre style="color: #fff">Getting names and sizes...</pre>` | ||||
| 	); | ||||
| 	let [about, sizes] = await Promise.all([ | ||||
| 		fetch_about(db, following, {}), | ||||
| 		getSizes(following), | ||||
| 	]); | ||||
| 	await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`); | ||||
| 	following.sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0)); | ||||
| 	for (let id of following) { | ||||
| 		tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`; | ||||
| 	} | ||||
| 	await app.setDocument( | ||||
| 		'<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' + | ||||
| 			tree + | ||||
| 			'</ul>\n</body>\n</html>' | ||||
| 	); | ||||
| } | ||||
|  | ||||
| main(); | ||||
							
								
								
									
										5
									
								
								apps/identity.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/identity.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "🪪", | ||||
| 	"previous": "&de7q4A59auHP/34bXgeNH05JZoxsGr5TjwXPvehWH30=.sha256" | ||||
| } | ||||
							
								
								
									
										134
									
								
								apps/identity/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								apps/identity/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| import * as tfrpc from '/tfrpc.js'; | ||||
|  | ||||
| tfrpc.register(async function get_private_key(id) { | ||||
| 	return bip39Words(await ssb.getPrivateKey(id)); | ||||
| }); | ||||
| tfrpc.register(async function create_id(id) { | ||||
| 	return await ssb.createIdentity(); | ||||
| }); | ||||
| tfrpc.register(async function add_id(id) { | ||||
| 	return await ssb.addIdentity(bip39Bytes(id)); | ||||
| }); | ||||
| tfrpc.register(async function delete_id(id) { | ||||
| 	return await ssb.deleteIdentity(id); | ||||
| }); | ||||
| tfrpc.register(async function reload() { | ||||
| 	await main(); | ||||
| }); | ||||
|  | ||||
| async function main() { | ||||
| 	let ids = await ssb.getIdentities(); | ||||
| 	await app.setDocument( | ||||
| 		` | ||||
| 		<head> | ||||
| 			<link rel="stylesheet" href="w3.css"></link> | ||||
| 			<style> | ||||
| 				/* "2018 Sargasso Sea" */ | ||||
| 				.w3-theme-l5 {color:#000 !important; background-color:#f3f4f7 !important} | ||||
| 				.w3-theme-l4 {color:#000 !important; background-color:#d7dbe3 !important} | ||||
| 				.w3-theme-l3 {color:#000 !important; background-color:#b0b6c8 !important} | ||||
| 				.w3-theme-l2 {color:#fff !important; background-color:#8892ac !important} | ||||
| 				.w3-theme-l1 {color:#fff !important; background-color:#636f8e !important} | ||||
| 				.w3-theme-d1 {color:#fff !important; background-color:#40485c !important} | ||||
| 				.w3-theme-d2 {color:#fff !important; background-color:#394052 !important} | ||||
| 				.w3-theme-d3 {color:#fff !important; background-color:#323848 !important} | ||||
| 				.w3-theme-d4 {color:#fff !important; background-color:#2b303d !important} | ||||
| 				.w3-theme-d5 {color:#fff !important; background-color:#242833 !important} | ||||
|  | ||||
| 				.w3-theme-light {color:#000 !important; background-color:#f3f4f7 !important} | ||||
| 				.w3-theme-dark {color:#fff !important; background-color:#242833 !important} | ||||
| 				.w3-theme-action {color:#fff !important; background-color:#242833 !important} | ||||
|  | ||||
| 				.w3-theme {color:#fff !important; background-color:#485167 !important} | ||||
| 				.w3-text-theme {color:#485167 !important} | ||||
| 				.w3-border-theme {border-color:#485167 !important} | ||||
|  | ||||
| 				.w3-hover-theme:hover {color:#fff !important; background-color:#485167 !important} | ||||
| 				.w3-hover-text-theme:hover {color:#485167 !important} | ||||
| 				.w3-hover-border-theme:hover {border-color:#485167 !important} | ||||
| 			</style> | ||||
| 		</head> | ||||
| 		<body class="w3-theme-l3"> | ||||
| 		<script>const handler = {};</script> | ||||
| 		<script type="module"> | ||||
| 			import * as tfrpc from '/static/tfrpc.js'; | ||||
| 			handler.export_id = async function export_id(event) { | ||||
| 				let id = event.srcElement.dataset.id; | ||||
| 				let element = document.createElement('textarea'); | ||||
| 				element.value = await tfrpc.rpc.get_private_key(id); | ||||
| 				element.style = 'width: 100%; height: auto; read-only: true; resize: none'; | ||||
| 				element.classList.add('w3-input'); | ||||
| 				element.readOnly = true; | ||||
| 				event.srcElement.parentElement.appendChild(element); | ||||
| 				event.srcElement.onclick = event => handler.hide_id(event, element); | ||||
| 			} | ||||
| 			handler.add_id = async function add_id(event) { | ||||
| 				let id = document.getElementById('add_id').value; | ||||
| 				try { | ||||
| 					let new_id = await tfrpc.rpc.add_id(id); | ||||
| 					alert('Successfully imported: ' + new_id); | ||||
| 					await tfrpc.rpc.reload(); | ||||
| 				} catch (e) { | ||||
| 					alert('Error importing identity: ' + e); | ||||
| 				} | ||||
| 			} | ||||
| 			handler.create_id = async function create_id(event) { | ||||
| 				try { | ||||
| 					let id = await tfrpc.rpc.create_id(); | ||||
| 					alert('Successfully created: ' + id); | ||||
| 					await tfrpc.rpc.reload(); | ||||
| 				} catch (e) { | ||||
| 					alert('Error creating identity: ' + e); | ||||
| 				} | ||||
| 			} | ||||
| 			handler.hide_id = function hide_id(event, element) { | ||||
| 				element.parentNode.removeChild(element); | ||||
| 				event.srcElement.onclick = handler.export_id; | ||||
| 			} | ||||
| 			handler.delete_id = async function delete_id(event) { | ||||
| 				let id = event.srcElement.dataset.id; | ||||
| 				try { | ||||
| 					if (prompt('Are you sure you want to delete "' + id + '"?  It cannot be recovered without the exported phrase.\\n\\nEnter the word "DELETE" to confirm you wish to delete it.') === 'DELETE') { | ||||
| 						if (await tfrpc.rpc.delete_id(id)) { | ||||
| 							alert('Successfully deleted ID: ' + id); | ||||
| 						} | ||||
| 						await tfrpc.rpc.reload(); | ||||
| 					} | ||||
| 				} catch (e) { | ||||
| 					alert('Error deleting ID: ' + e); | ||||
| 				} | ||||
| 			} | ||||
| 		</script> | ||||
| 		<header class="w3-theme w3-padding"><h1>SSB Identity Management</h1></header> | ||||
| 		<div class="w3-card-4 w3-margin"> | ||||
| 			<header class="w3-container w3-theme-l2"><h2>Create a new identity</h2></header> | ||||
| 			<footer class="w3-padding"> | ||||
| 				<button id="create_id" onclick="handler.create_id()" class="w3-button w3-theme">Create Identity</button> | ||||
| 			</footer> | ||||
| 		</div> | ||||
| 		<div class="w3-card-4 w3-margin"> | ||||
| 			<header class="w3-container w3-theme-l2"><h2>Import an SSB Identity from 12 BIP39 English Words</h2></header> | ||||
| 			<textarea id="add_id" style="width: 100%" rows="4" class="w3-input"></textarea> | ||||
| 			<footer class="w3-padding"> | ||||
| 				<button id="add" onclick="handler.add_id(event)" class="w3-button w3-theme">Import Identity</button> | ||||
| 			</footer> | ||||
| 		</div> | ||||
| 		<div class="w3-card-4 w3-margin"> | ||||
| 			<header class="w3-container w3-theme-l2"><h2>Identities</h2></header> | ||||
| 			<ul class="w3-ul">` + | ||||
| 				ids | ||||
| 					.map( | ||||
| 						(id) => `<li style="overflow: hidden; text-wrap: nowrap; text-overflow: ellipsis"> | ||||
| 				<button onclick="handler.export_id(event)" data-id="${id}" class="w3-button w3-theme">Export Identity</button> | ||||
| 				<button onclick="handler.delete_id(event)" data-id="${id}" class="w3-button w3-theme">Delete Identity</button> | ||||
| 				${id} | ||||
| 			</li>` | ||||
| 					) | ||||
| 					.join('\n') + | ||||
| 				`	</ul> | ||||
| 		</div> | ||||
| 	</body>` | ||||
| 	); | ||||
| } | ||||
|  | ||||
| main(); | ||||
							
								
								
									
										235
									
								
								apps/identity/w3.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								apps/identity/w3.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,235 @@ | ||||
| /* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */ | ||||
| html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit} | ||||
| /* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ | ||||
| html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} | ||||
| article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item} | ||||
| audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline} | ||||
| audio:not([controls]){display:none;height:0}[hidden],template{display:none} | ||||
| a{background-color:transparent}a:active,a:hover{outline-width:0} | ||||
| abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted} | ||||
| b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000} | ||||
| small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} | ||||
| sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none} | ||||
| code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible} | ||||
| button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold} | ||||
| button,input{overflow:visible}button,select{text-transform:none} | ||||
| button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button} | ||||
| button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0} | ||||
| button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText} | ||||
| fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em} | ||||
| legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto} | ||||
| [type=checkbox],[type=radio]{padding:0} | ||||
| [type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto} | ||||
| [type=search]{-webkit-appearance:textfield;outline-offset:-2px} | ||||
| [type=search]::-webkit-search-decoration{-webkit-appearance:none} | ||||
| ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit} | ||||
| /* End extract */ | ||||
| html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden} | ||||
| h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px} | ||||
| .w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace} | ||||
| h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px} | ||||
| hr{border:0;border-top:1px solid #eee;margin:20px 0} | ||||
| .w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit} | ||||
| .w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc} | ||||
| .w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1} | ||||
| .w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1} | ||||
| .w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center} | ||||
| .w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top} | ||||
| .w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px} | ||||
| .w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap} | ||||
| .w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)} | ||||
| .w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}    | ||||
| .w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none} | ||||
| .w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none} | ||||
| .w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%} | ||||
| .w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none} | ||||
| .w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block} | ||||
| .w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s} | ||||
| .w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%} | ||||
| .w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc} | ||||
| .w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer} | ||||
| .w3-dropdown-hover:hover .w3-dropdown-content{display:block} | ||||
| .w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000} | ||||
| .w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000} | ||||
| .w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1} | ||||
| .w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px} | ||||
| .w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto} | ||||
| .w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%} | ||||
| .w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%} | ||||
| .w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px} | ||||
| .w3-main,#main{transition:margin-left .4s} | ||||
| .w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)} | ||||
| .w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px} | ||||
| .w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto} | ||||
| .w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0} | ||||
| .w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left} | ||||
| .w3-bar .w3-button{white-space:normal} | ||||
| .w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0} | ||||
| .w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%} | ||||
| .w3-responsive{display:block;overflow-x:auto} | ||||
| .w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before, | ||||
| .w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both} | ||||
| .w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%} | ||||
| .w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%} | ||||
| .w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%} | ||||
| .w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%} | ||||
| @media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%} | ||||
| .w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%} | ||||
| .w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}} | ||||
| @media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%} | ||||
| .w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%} | ||||
| .w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}} | ||||
| .w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px} | ||||
| .w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px} | ||||
| .w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell} | ||||
| .w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom} | ||||
| .w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important} | ||||
| @media (max-width:1205px){.w3-auto{max-width:95%}} | ||||
| @media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px} | ||||
| .w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}	 | ||||
| .w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center} | ||||
| .w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}} | ||||
| @media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}} | ||||
| @media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}} | ||||
| @media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}} | ||||
| @media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}} | ||||
| .w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0} | ||||
| .w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2} | ||||
| .w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0} | ||||
| .w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0} | ||||
| .w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)} | ||||
| .w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)} | ||||
| .w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)} | ||||
| .w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} | ||||
| .w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} | ||||
| .w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none} | ||||
| .w3-display-position{position:absolute} | ||||
| .w3-circle{border-radius:50%} | ||||
| .w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px} | ||||
| .w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px} | ||||
| .w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px} | ||||
| .w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px} | ||||
| .w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word} | ||||
| .w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%} | ||||
| .w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)} | ||||
| .w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)} | ||||
| .w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}} | ||||
| .w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} | ||||
| .w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}} | ||||
| .w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}} | ||||
| .w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}} | ||||
| .w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}} | ||||
| .w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}} | ||||
| .w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}} | ||||
| .w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important} | ||||
| .w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1} | ||||
| .w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75} | ||||
| .w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)} | ||||
| .w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)} | ||||
| .w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)} | ||||
| .w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important} | ||||
| .w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important} | ||||
| .w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important} | ||||
| .w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important} | ||||
| .w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important} | ||||
| .w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important} | ||||
| .w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important} | ||||
| .w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important} | ||||
| .w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important} | ||||
| .w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important} | ||||
| .w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important} | ||||
| .w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important} | ||||
| .w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important} | ||||
| .w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important} | ||||
| .w3-padding-64{padding-top:64px!important;padding-bottom:64px!important} | ||||
| .w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important} | ||||
| .w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important} | ||||
| .w3-left{float:left!important}.w3-right{float:right!important} | ||||
| .w3-button:hover{color:#000!important;background-color:#ccc!important} | ||||
| .w3-transparent,.w3-hover-none:hover{background-color:transparent!important} | ||||
| .w3-hover-none:hover{box-shadow:none!important} | ||||
| /* Colors */ | ||||
| .w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important} | ||||
| .w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important} | ||||
| .w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important} | ||||
| .w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important} | ||||
| .w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important} | ||||
| .w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important} | ||||
| .w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important} | ||||
| .w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important} | ||||
| .w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important} | ||||
| .w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important} | ||||
| .w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important} | ||||
| .w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important} | ||||
| .w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important} | ||||
| .w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important} | ||||
| .w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important} | ||||
| .w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important} | ||||
| .w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important} | ||||
| .w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important} | ||||
| .w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important} | ||||
| .w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important} | ||||
| .w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important} | ||||
| .w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important} | ||||
| .w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important} | ||||
| .w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important} | ||||
| .w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important} | ||||
| .w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important} | ||||
| .w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important} | ||||
| .w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important} | ||||
| .w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important} | ||||
| .w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important} | ||||
| .w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important} | ||||
| .w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important} | ||||
| .w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important} | ||||
| .w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important} | ||||
| .w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important} | ||||
| .w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important} | ||||
| .w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important} | ||||
| .w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important} | ||||
| .w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important} | ||||
| .w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important} | ||||
| .w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important} | ||||
| .w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important} | ||||
| .w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important} | ||||
| .w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important} | ||||
| .w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important} | ||||
| .w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important} | ||||
| .w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important} | ||||
| .w3-text-red,.w3-hover-text-red:hover{color:#f44336!important} | ||||
| .w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important} | ||||
| .w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important} | ||||
| .w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important} | ||||
| .w3-text-white,.w3-hover-text-white:hover{color:#fff!important} | ||||
| .w3-text-black,.w3-hover-text-black:hover{color:#000!important} | ||||
| .w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important} | ||||
| .w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important} | ||||
| .w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important} | ||||
| .w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important} | ||||
| .w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important} | ||||
| .w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important} | ||||
| .w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important} | ||||
| .w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important} | ||||
| .w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important} | ||||
| .w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important} | ||||
| .w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important} | ||||
| .w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important} | ||||
| .w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important} | ||||
| .w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important} | ||||
| .w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important} | ||||
| .w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important} | ||||
| .w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important} | ||||
| .w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important} | ||||
| .w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important} | ||||
| .w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important} | ||||
| .w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important} | ||||
| .w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important} | ||||
| .w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important} | ||||
| .w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important} | ||||
| .w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important} | ||||
| .w3-border-black,.w3-hover-border-black:hover{border-color:#000!important} | ||||
| .w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important} | ||||
| .w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important} | ||||
| .w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important} | ||||
| .w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important} | ||||
| .w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important} | ||||
							
								
								
									
										5
									
								
								apps/issues.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/issues.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "🦟", | ||||
| 	"previous": "&cUqvSDUls3jn0haD85LPFAGdkc8wFuy347TtATNcJgg=.sha256" | ||||
| } | ||||
							
								
								
									
										108
									
								
								apps/issues/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								apps/issues/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| 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(async function store_message(message) { | ||||
| 	return await ssb.storeMessage(message); | ||||
| }); | ||||
| tfrpc.register(function apps() { | ||||
| 	return core.apps(); | ||||
| }); | ||||
| tfrpc.register(function getActiveIdentity() { | ||||
| 	return ssb.getActiveIdentity(); | ||||
| }); | ||||
| tfrpc.register(async function try_decrypt(id, content) { | ||||
| 	return await ssb.privateMessageDecrypt(id, content); | ||||
| }); | ||||
| 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(); | ||||
							
								
								
									
										91
									
								
								apps/issues/commonmark-linkify.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								apps/issues/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/issues/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/issues/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										16
									
								
								apps/issues/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								apps/issues/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| <!doctype html> | ||||
| <html style="color: #fff"> | ||||
| 	<head> | ||||
| 		<title>Tilde Friends</title> | ||||
| 		<base target="_top" /> | ||||
| 	</head> | ||||
| 	<body> | ||||
| 		<tf-issues-app /> | ||||
| 		<script> | ||||
| 			window.litDisableBundleWarning = true; | ||||
| 		</script> | ||||
| 		<script src="commonmark.min.js"></script> | ||||
| 		<script src="commonmark-linkify.js" type="module"></script> | ||||
| 		<script src="script.js" type="module"></script> | ||||
| 	</body> | ||||
| </html> | ||||
							
								
								
									
										120
									
								
								apps/issues/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								apps/issues/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								apps/issues/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/issues/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										248
									
								
								apps/issues/script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								apps/issues/script.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,248 @@ | ||||
| import {LitElement, html, unsafeHTML} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
| import * as tfutils from './tf-utils.js'; | ||||
|  | ||||
| const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256'; | ||||
|  | ||||
| class TfComposeElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			value: {type: String}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	input() { | ||||
| 		let input = this.renderRoot.getElementById('input'); | ||||
| 		let preview = this.renderRoot.getElementById('preview'); | ||||
| 		if (input && preview) { | ||||
| 			preview.innerHTML = tfutils.markdown(input.value); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	submit() { | ||||
| 		this.dispatchEvent( | ||||
| 			new CustomEvent('tf-submit', { | ||||
| 				bubbles: true, | ||||
| 				composed: true, | ||||
| 				detail: { | ||||
| 					value: this.renderRoot.getElementById('input').value, | ||||
| 				}, | ||||
| 			}) | ||||
| 		); | ||||
| 		this.renderRoot.getElementById('input').value = ''; | ||||
| 		this.input(); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		return html` | ||||
| 			<div style="display: flex; flex-direction: row"> | ||||
| 				<textarea id="input" @input=${this.input} style="flex: 1 1">${this.value}</textarea> | ||||
| 				<div id="preview" style="flex: 1 1"></div> | ||||
| 			</div> | ||||
| 			<input type="submit" value="Submit" @click=${this.submit}></input> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
| customElements.define('tf-compose', TfComposeElement); | ||||
|  | ||||
| class TfIssuesAppElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			issues: {type: Array}, | ||||
| 			selected: {type: Object}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.issues = []; | ||||
| 		this.load(); | ||||
| 	} | ||||
|  | ||||
| 	async load() { | ||||
| 		let issues = {}; | ||||
| 		let messages = await tfrpc.rpc.query( | ||||
| 			` | ||||
| 			WITH issues AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM messages_refs JOIN messages ON | ||||
| 				messages.id = messages_refs.message | ||||
| 				WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'), | ||||
| 			edits AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM issues JOIN messages_refs ON | ||||
| 				issues.id = messages_refs.ref JOIN messages ON | ||||
| 				messages.id = messages_refs.message | ||||
| 				WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post')) | ||||
| 			SELECT * FROM issues | ||||
| 			UNION | ||||
| 			SELECT * FROM edits ORDER BY timestamp | ||||
| 		`, | ||||
| 			[k_project] | ||||
| 		); | ||||
| 		for (let message of messages) { | ||||
| 			let content = JSON.parse(message.content); | ||||
| 			switch (content.type) { | ||||
| 				case 'issue': | ||||
| 					issues[message.id] = { | ||||
| 						id: message.id, | ||||
| 						author: message.author, | ||||
| 						text: content.text, | ||||
| 						updates: [], | ||||
| 						created: message.timestamp, | ||||
| 						open: true, | ||||
| 					}; | ||||
| 					break; | ||||
| 				case 'issue-edit': | ||||
| 				case 'post': | ||||
| 					for (let issue of content.issues || []) { | ||||
| 						if (issues[issue.link]) { | ||||
| 							if (issue.open !== undefined) { | ||||
| 								issues[issue.link].open = issue.open; | ||||
| 								message.open = issue.open; | ||||
| 							} | ||||
| 							issues[issue.link].updates.push(message); | ||||
| 							issues[issue.link].updated = message.timestamp; | ||||
| 						} | ||||
| 					} | ||||
| 					break; | ||||
| 			} | ||||
| 		} | ||||
| 		this.issues = Object.values(issues).sort( | ||||
| 			(x, y) => y.open - x.open || y.created - x.created | ||||
| 		); | ||||
| 		if (this.selected) { | ||||
| 			for (let issue of this.issues) { | ||||
| 				if (issue.id == this.selected.id) { | ||||
| 					this.selected = issue; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render_issue_table_row(issue) { | ||||
| 		return html` | ||||
| 			<tr> | ||||
| 				<td>${issue.open ? '☐ open' : '☑ closed'}</td> | ||||
| 				<td | ||||
| 					style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis" | ||||
| 				> | ||||
| 					${issue.author} | ||||
| 				</td> | ||||
| 				<td | ||||
| 					style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer" | ||||
| 					@click=${() => (this.selected = issue)} | ||||
| 				> | ||||
| 					${issue.text.split('\n')?.[0]} | ||||
| 				</td> | ||||
| 				<td> | ||||
| 					${new Date(issue.updated ?? issue.created).toLocaleDateString()} | ||||
| 				</td> | ||||
| 			</tr> | ||||
| 		`; | ||||
| 	} | ||||
|  | ||||
| 	render_update(update) { | ||||
| 		let content = JSON.parse(update.content); | ||||
| 		let message; | ||||
| 		if (content.text) { | ||||
| 			message = unsafeHTML(tfutils.markdown(content.text)); | ||||
| 		} | ||||
| 		return html` | ||||
| 			<div style="border-left: 2px solid #fff; padding-left: 8px"> | ||||
| 				<div>${new Date(update.timestamp).toLocaleString()}</div> | ||||
| 				<div>${update.author}</div> | ||||
| 				<div>${message}</div> | ||||
| 				<div> | ||||
| 					${update.open !== undefined | ||||
| 						? update.open | ||||
| 							? 'issue opened' | ||||
| 							: 'issue closed' | ||||
| 						: undefined} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		`; | ||||
| 	} | ||||
|  | ||||
| 	async set_open(id, open) { | ||||
| 		if ( | ||||
| 			confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`) | ||||
| 		) { | ||||
| 			let whoami = await tfrpc.rpc.getActiveIdentity(); | ||||
| 			await tfrpc.rpc.appendMessage(whoami, { | ||||
| 				type: 'issue-edit', | ||||
| 				issues: [ | ||||
| 					{ | ||||
| 						link: id, | ||||
| 						open: open, | ||||
| 					}, | ||||
| 				], | ||||
| 			}); | ||||
| 			await this.load(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async create_issue(event) { | ||||
| 		let whoami = await tfrpc.rpc.getActiveIdentity(); | ||||
| 		await tfrpc.rpc.appendMessage(whoami, { | ||||
| 			type: 'issue', | ||||
| 			project: k_project, | ||||
| 			text: event.detail.value, | ||||
| 		}); | ||||
| 		await this.load(); | ||||
| 	} | ||||
|  | ||||
| 	async reply_to_issue(event) { | ||||
| 		let whoami = await tfrpc.rpc.getActiveIdentity(); | ||||
| 		await tfrpc.rpc.appendMessage(whoami, { | ||||
| 			type: 'post', | ||||
| 			text: event.detail.value, | ||||
| 			root: this.selected.id, | ||||
| 			branch: this.selected.updates.length | ||||
| 				? this.selected.updates[this.selected.updates.length - 1].id | ||||
| 				: this.selected.id, | ||||
| 			issues: [ | ||||
| 				{ | ||||
| 					link: this.selected.id, | ||||
| 				}, | ||||
| 			], | ||||
| 		}); | ||||
| 		await this.load(); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let header = html` <h1>Tilde Friends Issues</h1> `; | ||||
| 		if (this.selected) { | ||||
| 			return html` | ||||
| 				${header} | ||||
| 				<div> | ||||
| 					<input type="button" value="Back" @click=${() => (this.selected = undefined)}></input> | ||||
| 					${ | ||||
| 						this.selected.open | ||||
| 							? html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>` | ||||
| 							: html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>` | ||||
| 					} | ||||
| 				</div> | ||||
| 				<div>${new Date(this.selected.created).toLocaleString()}</div> | ||||
| 				<div>${this.selected.author}</div> | ||||
| 				<div>${this.selected.id}</div> | ||||
| 				<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div> | ||||
| 				${this.selected.updates.map((x) => this.render_update(x))} | ||||
| 				<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose> | ||||
| 			`; | ||||
| 		} else { | ||||
| 			return html` | ||||
| 				${header} | ||||
| 				<h2>New Issue</h2> | ||||
| 				<tf-compose @tf-submit=${this.create_issue}></tf-compose> | ||||
| 				<table> | ||||
| 					<tr> | ||||
| 						<th>Status</th> | ||||
| 						<th>Author</th> | ||||
| 						<th>Title</th> | ||||
| 						<th>Date</th> | ||||
| 					</tr> | ||||
| 					${this.issues.map((x) => this.render_issue_table_row(x))} | ||||
| 				</table> | ||||
| 			`; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-issues-app', TfIssuesAppElement); | ||||
							
								
								
									
										113
									
								
								apps/issues/tf-utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								apps/issues/tf-utils.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| import * as linkify from './commonmark-linkify.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); | ||||
| 	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}`; | ||||
| } | ||||
							
								
								
									
										5
									
								
								apps/journal.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/journal.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "📝", | ||||
| 	"previous": "&b//KqE4Vx6kOSBRODK1p/8wjOLKZJ+CBB5IkaBt5YsM=.sha256" | ||||
| } | ||||
							
								
								
									
										185
									
								
								apps/journal/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								apps/journal/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,185 @@ | ||||
| import * as tfrpc from '/tfrpc.js'; | ||||
|  | ||||
| let g_hash; | ||||
| let g_collection_notifies = {}; | ||||
|  | ||||
| tfrpc.register(async function getOwnerIdentities() { | ||||
| 	return ssb.getOwnerIdentities(); | ||||
| }); | ||||
|  | ||||
| tfrpc.register(async function getIdentities() { | ||||
| 	return ssb.getIdentities(); | ||||
| }); | ||||
|  | ||||
| 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 localStorageGet(key) { | ||||
| 	return app.localStorageGet(key); | ||||
| }); | ||||
|  | ||||
| tfrpc.register(async function localStorageSet(key, value) { | ||||
| 	return app.localStorageSet(key, value); | ||||
| }); | ||||
|  | ||||
| tfrpc.register(async function following(ids, depth) { | ||||
| 	return ssb.following(ids, depth); | ||||
| }); | ||||
|  | ||||
| tfrpc.register(async function appendMessage(id, message) { | ||||
| 	return ssb.appendMessageWithIdentity(id, message); | ||||
| }); | ||||
|  | ||||
| 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)); | ||||
| }); | ||||
|  | ||||
| let g_new_message_resolve; | ||||
| let g_new_message_promise = new Promise(function (resolve, reject) { | ||||
| 	g_new_message_resolve = resolve; | ||||
| }); | ||||
|  | ||||
| function new_message() { | ||||
| 	return g_new_message_promise; | ||||
| } | ||||
|  | ||||
| ssb.addEventListener('message', function (id) { | ||||
| 	let resolve = g_new_message_resolve; | ||||
| 	g_new_message_promise = new Promise(function (resolve, reject) { | ||||
| 		g_new_message_resolve = resolve; | ||||
| 	}); | ||||
| 	if (resolve) { | ||||
| 		resolve(); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| core.register('message', async function message_handler(message) { | ||||
| 	if (message.event == 'hashChange') { | ||||
| 		print('hash change', message.hash); | ||||
| 		g_hash = message.hash; | ||||
| 		await tfrpc.rpc.hash_changed(message.hash); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| tfrpc.register(function set_hash(hash) { | ||||
| 	if (g_hash != hash) { | ||||
| 		return app.setHash(hash); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| tfrpc.register(function get_hash(id, message) { | ||||
| 	return g_hash; | ||||
| }); | ||||
|  | ||||
| tfrpc.register(async function try_decrypt(id, content) { | ||||
| 	return await ssb.privateMessageDecrypt(id, content); | ||||
| }); | ||||
| tfrpc.register(async function encrypt(id, recipients, content) { | ||||
| 	return await ssb.privateMessageEncrypt(id, recipients, content); | ||||
| }); | ||||
|  | ||||
| async function process_message(whoami, collection, message, kind, parent) { | ||||
| 	let content = JSON.parse(message.content); | ||||
| 	if (typeof content == 'string') { | ||||
| 		let x; | ||||
| 		for (let id of whoami) { | ||||
| 			x = await ssb.privateMessageDecrypt(id, content); | ||||
| 			if (x) { | ||||
| 				content = JSON.parse(x); | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
| 		if (!x) { | ||||
| 			return; | ||||
| 		} | ||||
| 		if (content.type !== kind || (parent && content.parent !== parent)) { | ||||
| 			return; | ||||
| 		} | ||||
| 	} | ||||
| 	if (content?.key) { | ||||
| 		if (content?.tombstone) { | ||||
| 			delete collection[content.key]; | ||||
| 		} else { | ||||
| 			collection[content.key] = Object.assign( | ||||
| 				collection[content.key] || {}, | ||||
| 				content | ||||
| 			); | ||||
| 		} | ||||
| 	} else { | ||||
| 		collection[message.id] = Object.assign(content, {id: message.id}); | ||||
| 	} | ||||
| 	return true; | ||||
| } | ||||
|  | ||||
| tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) { | ||||
| 	let whoami = await ssb.getIdentities(); | ||||
| 	data = data ?? {}; | ||||
| 	let rowid = 0; | ||||
| 	await ssb.sqlAsync( | ||||
| 		'SELECT MAX(rowid) AS rowid FROM messages', | ||||
| 		[], | ||||
| 		function (row) { | ||||
| 			rowid = row.rowid; | ||||
| 		} | ||||
| 	); | ||||
| 	while (true) { | ||||
| 		if (rowid == max_rowid) { | ||||
| 			await new_message(); | ||||
| 			await ssb.sqlAsync( | ||||
| 				'SELECT MAX(rowid) AS rowid FROM messages', | ||||
| 				[], | ||||
| 				function (row) { | ||||
| 					rowid = row.rowid; | ||||
| 				} | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		let modified = false; | ||||
| 		let rows = []; | ||||
| 		await ssb.sqlAsync( | ||||
| 			` | ||||
| 			SELECT messages.id, author, content, timestamp | ||||
| 			FROM messages | ||||
| 			JOIN json_each(?1) AS id ON messages.author = id.value | ||||
| 			WHERE | ||||
| 				messages.rowid > ?2 AND | ||||
| 				messages.rowid <= ?3 AND | ||||
| 				((json_extract(messages.content, '$.type') = ?4 AND | ||||
| 				(?5 IS NULL OR json_extract(messages.content, '$.parent') = ?5)) OR | ||||
| 				content LIKE '"%') | ||||
| 			`, | ||||
| 			[JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent], | ||||
| 			function (row) { | ||||
| 				rows.push(row); | ||||
| 			} | ||||
| 		); | ||||
| 		max_rowid = rowid; | ||||
| 		for (let row of rows) { | ||||
| 			if (await process_message(whoami, data, row, kind, parent)) { | ||||
| 				modified = true; | ||||
| 			} | ||||
| 		} | ||||
| 		if (modified) { | ||||
| 			break; | ||||
| 		} | ||||
| 	} | ||||
| 	return [rowid, data]; | ||||
| }); | ||||
|  | ||||
| async function main() { | ||||
| 	await app.setDocument(utf8Decode(await getFile('index.html'))); | ||||
| } | ||||
|  | ||||
| main(); | ||||
							
								
								
									
										1
									
								
								apps/journal/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/journal/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										16
									
								
								apps/journal/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								apps/journal/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| <!doctype html> | ||||
| <html> | ||||
| 	<head> | ||||
| 		<base target="_top" /> | ||||
| 	</head> | ||||
| 	<body style="color: #fff"> | ||||
| 		<tf-journal-app></tf-journal-app> | ||||
| 		<script src="commonmark.min.js"></script> | ||||
| 		<script> | ||||
| 			window.litDisableBundleWarning = true; | ||||
| 		</script> | ||||
| 		<script src="tf-journal-app.js" type="module"></script> | ||||
| 		<script src="tf-journal-entry.js" type="module"></script> | ||||
| 		<script src="tf-id-picker.js" type="module"></script> | ||||
| 	</body> | ||||
| </html> | ||||
							
								
								
									
										120
									
								
								apps/journal/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								apps/journal/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								apps/journal/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/journal/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										43
									
								
								apps/journal/tf-id-picker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								apps/journal/tf-id-picker.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| 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(); | ||||
| 		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); | ||||
							
								
								
									
										89
									
								
								apps/journal/tf-journal-app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								apps/journal/tf-journal-app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| import {LitElement, html, keyed, live} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| class TfJournalAppElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			ids: {type: Array}, | ||||
| 			owner_ids: {type: Array}, | ||||
| 			whoami: {type: String}, | ||||
| 			journals: {type: Object}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.ids = []; | ||||
| 		this.owner_ids = []; | ||||
| 		this.journals = {}; | ||||
| 		this.load(); | ||||
| 	} | ||||
|  | ||||
| 	async load() { | ||||
| 		this.ids = await tfrpc.rpc.getIdentities(); | ||||
| 		this.whoami = await tfrpc.rpc.localStorageGet('journal_whoami'); | ||||
| 		await this.read_journals(); | ||||
| 	} | ||||
|  | ||||
| 	async read_journals() { | ||||
| 		let max_rowid; | ||||
| 		let journals; | ||||
| 		while (true) { | ||||
| 			[max_rowid, journals] = await tfrpc.rpc.collection( | ||||
| 				[this.whoami], | ||||
| 				'journal-entry', | ||||
| 				undefined, | ||||
| 				max_rowid, | ||||
| 				journals | ||||
| 			); | ||||
| 			this.journals = Object.assign({}, journals); | ||||
| 			console.log('JOURNALS', this.journals); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async on_whoami_changed(event) { | ||||
| 		let new_id = event.srcElement.selected; | ||||
| 		await tfrpc.rpc.localStorageSet('journal_whoami', new_id); | ||||
| 		this.whoami = new_id; | ||||
| 	} | ||||
|  | ||||
| 	async on_journal_publish(event) { | ||||
| 		let key = event.detail.key; | ||||
| 		let text = event.detail.text; | ||||
| 		let message = { | ||||
| 			type: 'journal-entry', | ||||
| 			key: key, | ||||
| 			text: text, | ||||
| 		}; | ||||
| 		message.recps = [this.whoami]; | ||||
| 		print(message); | ||||
| 		message = await tfrpc.rpc.encrypt( | ||||
| 			this.whoami, | ||||
| 			message.recps, | ||||
| 			JSON.stringify(message) | ||||
| 		); | ||||
| 		print(message); | ||||
| 		await tfrpc.rpc.appendMessage(this.whoami, message); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		console.log('RENDER APP', this.journals); | ||||
| 		let self = this; | ||||
| 		return html` | ||||
| 			<div> | ||||
| 				<tf-id-picker | ||||
| 					.ids=${this.ids} | ||||
| 					selected=${this.whoami} | ||||
| 					@change=${this.on_whoami_changed} | ||||
| 				></tf-id-picker> | ||||
| 			</div> | ||||
| 			<tf-journal-entry | ||||
| 				whoami=${this.whoami} | ||||
| 				.journals=${this.journals} | ||||
| 				@publish=${this.on_journal_publish} | ||||
| 			></tf-journal-entry> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-journal-app', TfJournalAppElement); | ||||
							
								
								
									
										97
									
								
								apps/journal/tf-journal-entry.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								apps/journal/tf-journal-entry.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| import {LitElement, html, unsafeHTML, range} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| class TfJournalEntryElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			whoami: {type: String}, | ||||
| 			key: {type: String}, | ||||
| 			journals: {type: Object}, | ||||
| 			text: {type: String}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.journals = {}; | ||||
| 		this.key = new Date().toISOString().split('T')[0]; | ||||
| 	} | ||||
|  | ||||
| 	markdown(md) { | ||||
| 		var reader = new commonmark.Parser({safe: true}); | ||||
| 		var writer = new commonmark.HtmlRenderer(); | ||||
| 		var parsed = reader.parse(md || ''); | ||||
| 		return writer.render(parsed); | ||||
| 	} | ||||
|  | ||||
| 	on_discard(event) { | ||||
| 		this.text = undefined; | ||||
| 	} | ||||
|  | ||||
| 	async on_publish() { | ||||
| 		console.log('publish', this.text); | ||||
| 		this.dispatchEvent( | ||||
| 			new CustomEvent('publish', { | ||||
| 				bubbles: true, | ||||
| 				detail: { | ||||
| 					key: this.shadowRoot.getElementById('date_picker').value, | ||||
| 					text: this.text, | ||||
| 				}, | ||||
| 			}) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	back_dates(count) { | ||||
| 		let now = new Date(); | ||||
| 		let result = []; | ||||
| 		for (let i = 0; i < count; i++) { | ||||
| 			let next = new Date(now); | ||||
| 			next.setDate(now.getDate() - i); | ||||
| 			result.push(next.toISOString().split('T')[0]); | ||||
| 		} | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	on_edit(event) { | ||||
| 		this.text = event.srcElement.value; | ||||
| 	} | ||||
|  | ||||
| 	on_date_change(event) { | ||||
| 		this.key = event.srcElement.value; | ||||
| 		this.text = this.journals[this.key]?.text; | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		console.log('RENDER ENTRY', this.key, this.journals?.[this.key]); | ||||
| 		return html` | ||||
| 			<select id="date_picker" @change=${this.on_date_change}> | ||||
| 				${this.back_dates(10).map( | ||||
| 					(x) => html` <option value=${x}>${x}</option> ` | ||||
| 				)} | ||||
| 			</select> | ||||
| 			<div style="display: inline-flex; flex-direction: row"> | ||||
| 				<button | ||||
| 					?disabled=${this.text == this.journals?.[this.key]?.text} | ||||
| 					@click=${this.on_publish} | ||||
| 				> | ||||
| 					Publish | ||||
| 				</button> | ||||
| 				<button @click=${this.on_discard}>Discard</button> | ||||
| 			</div> | ||||
| 			<div style="display: flex; flex-direction: row"> | ||||
| 				<textarea | ||||
| 					style="flex: 1 1; min-height: 10em" | ||||
| 					@input=${this.on_edit} | ||||
| 					.value=${this.text ?? this.journals?.[this.key]?.text ?? ''} | ||||
| 				></textarea> | ||||
| 				<div style="flex: 1 1"> | ||||
| 					${unsafeHTML( | ||||
| 						this.markdown(this.text ?? this.journals?.[this.key]?.text) | ||||
| 					)} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-journal-entry', TfJournalEntryElement); | ||||
							
								
								
									
										5
									
								
								apps/room.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/room.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "🚪", | ||||
| 	"previous": "&HXCdDG8gGYXElTyEFbg85jqa6lDXNL2ENPIA9UoJNbI=.sha256" | ||||
| } | ||||
							
								
								
									
										13
									
								
								apps/room/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								apps/room/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| async function main() { | ||||
| 	let host = core.url.match(/.*\/\/(.*?)\//)[1]; | ||||
| 	let id = (await ssb.getServerIdentity()).substring(1); | ||||
| 	let room = `net:${host}:${ssb.port}~shs:${id}:SSB+Room+SK3TLYC2T86EHQCUHBUHASCASE18JBV24=`; | ||||
| 	await app.setDocument(` | ||||
| 		<body style="color: #fff"> | ||||
| 			<h1>Server</h1> | ||||
| 			<div>The local server address is:</div> | ||||
| 			<div><input type="text" readonly value="${room}" style="width: 100%"></input></div> | ||||
| 		</body> | ||||
| 	`); | ||||
| } | ||||
| main(); | ||||
							
								
								
									
										5
									
								
								apps/sneaker.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/sneaker.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "👟", | ||||
| 	"previous": "&lYZRnT2UGQxXxYISbuaZewik9AuxBpcJumakwrePw5c=.sha256" | ||||
| } | ||||
							
								
								
									
										30
									
								
								apps/sneaker/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								apps/sneaker/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import * as tfrpc from '/tfrpc.js'; | ||||
|  | ||||
| tfrpc.register(async function getAllIdentities() { | ||||
| 	return ssb.getAllIdentities(); | ||||
| }); | ||||
| 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 store_blob(blob) { | ||||
| 	if (Array.isArray(blob)) { | ||||
| 		blob = Uint8Array.from(blob); | ||||
| 	} | ||||
| 	return await ssb.blobStore(blob); | ||||
| }); | ||||
| tfrpc.register(async function get_blob(id) { | ||||
| 	return Array.from(new Uint8Array(await ssb.blobGet(id))); | ||||
| }); | ||||
| tfrpc.register(async function store_message(message) { | ||||
| 	return await ssb.storeMessage(message); | ||||
| }); | ||||
|  | ||||
| async function main() { | ||||
| 	await app.setDocument(utf8Decode(await getFile('index.html'))); | ||||
| } | ||||
| main(); | ||||
							
								
								
									
										3
									
								
								apps/sneaker/filesaver.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								apps/sneaker/filesaver.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| (function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)}); | ||||
|  | ||||
| //# sourceMappingURL=FileSaver.min.js.map | ||||
							
								
								
									
										16
									
								
								apps/sneaker/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								apps/sneaker/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| <!doctype html> | ||||
| <html style="color: #fff"> | ||||
| 	<head> | ||||
| 		<title>Tilde Friends</title> | ||||
| 		<base target="_top" /> | ||||
| 	</head> | ||||
| 	<body> | ||||
| 		<tf-sneaker-app /> | ||||
| 		<script> | ||||
| 			window.litDisableBundleWarning = true; | ||||
| 		</script> | ||||
| 		<script src="filesaver.min.js"></script> | ||||
| 		<script src="jszip.min.js"></script> | ||||
| 		<script src="script.js" type="module"></script> | ||||
| 	</body> | ||||
| </html> | ||||
							
								
								
									
										13
									
								
								apps/sneaker/jszip.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								apps/sneaker/jszip.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										120
									
								
								apps/sneaker/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								apps/sneaker/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								apps/sneaker/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/sneaker/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										350
									
								
								apps/sneaker/script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										350
									
								
								apps/sneaker/script.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,350 @@ | ||||
| import {LitElement, html} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| class TfSneakerAppElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			feeds: {type: Object}, | ||||
| 			progress: {type: Object}, | ||||
| 			result: {type: String}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.feeds = []; | ||||
| 		this.progress = undefined; | ||||
| 		this.result = undefined; | ||||
| 	} | ||||
|  | ||||
| 	async search() { | ||||
| 		let q = this.renderRoot.getElementById('search').value; | ||||
| 		let result = await tfrpc.rpc.query( | ||||
| 			` | ||||
| 			SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name | ||||
| 			FROM messages_fts(?) | ||||
| 			JOIN messages ON messages.rowid = messages_fts.rowid | ||||
| 			WHERE | ||||
| 				json_extract(messages.content, '$.type') = 'about' AND | ||||
| 				json_extract(messages.content, '$.about') = messages.author AND | ||||
| 				json_extract(messages.content, '$.name') IS NOT NULL | ||||
| 			GROUP BY messages.author | ||||
| 			HAVING MAX(messages.sequence) | ||||
| 			ORDER BY COUNT(*) DESC | ||||
| 			`, | ||||
| 			[`"${q.replaceAll('"', '""')}"`] | ||||
| 		); | ||||
| 		this.feeds = Object.fromEntries(result.map((x) => [x.id, x.name])); | ||||
| 	} | ||||
|  | ||||
| 	format_message(message) { | ||||
| 		const k_flag_sequence_before_author = 1; | ||||
| 		let out = { | ||||
| 			previous: message.previous ?? null, | ||||
| 		}; | ||||
| 		if (message.flags & k_flag_sequence_before_author) { | ||||
| 			out.sequence = message.sequence; | ||||
| 			out.author = message.author; | ||||
| 		} else { | ||||
| 			out.author = message.author; | ||||
| 			out.sequence = message.sequence; | ||||
| 		} | ||||
| 		out.timestamp = message.timestamp; | ||||
| 		out.hash = message.hash; | ||||
| 		out.content = JSON.parse(message.content); | ||||
| 		out.signature = message.signature; | ||||
| 		return {key: message.id, value: out}; | ||||
| 	} | ||||
|  | ||||
| 	sanitize(value) { | ||||
| 		return value.replaceAll('/', '_').replaceAll('+', '-'); | ||||
| 	} | ||||
|  | ||||
| 	guess_ext(data) { | ||||
| 		function startsWith(prefix) { | ||||
| 			if (data.length < prefix.length) { | ||||
| 				return false; | ||||
| 			} | ||||
| 			for (let i = 0; i < prefix.length; i++) { | ||||
| 				if (prefix[i] !== null && data[i] !== prefix[i]) { | ||||
| 					return false; | ||||
| 				} | ||||
| 			} | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		if ( | ||||
| 			startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) || | ||||
| 			startsWith( | ||||
| 				data, | ||||
| 				[0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01] | ||||
| 			) || | ||||
| 			startsWith(data, [0xff, 0xd8, 0xff, 0xee]) || | ||||
| 			startsWith(data, [ | ||||
| 				0xff, | ||||
| 				0xd8, | ||||
| 				0xff, | ||||
| 				0xe1, | ||||
| 				null, | ||||
| 				null, | ||||
| 				0x45, | ||||
| 				0x78, | ||||
| 				0x69, | ||||
| 				0x66, | ||||
| 				0x00, | ||||
| 				0x00, | ||||
| 			]) | ||||
| 		) { | ||||
| 			return '.jpg'; | ||||
| 		} else if ( | ||||
| 			startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) | ||||
| 		) { | ||||
| 			return '.png'; | ||||
| 		} else if ( | ||||
| 			startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) || | ||||
| 			startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]) | ||||
| 		) { | ||||
| 			return '.gif'; | ||||
| 		} else if ( | ||||
| 			startsWith(data, [ | ||||
| 				0x52, | ||||
| 				0x49, | ||||
| 				0x46, | ||||
| 				0x46, | ||||
| 				null, | ||||
| 				null, | ||||
| 				null, | ||||
| 				null, | ||||
| 				0x57, | ||||
| 				0x45, | ||||
| 				0x42, | ||||
| 				0x50, | ||||
| 			]) | ||||
| 		) { | ||||
| 			return '.webp'; | ||||
| 		} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) { | ||||
| 			return '.svg'; | ||||
| 		} else if ( | ||||
| 			startsWith(data, [ | ||||
| 				null, | ||||
| 				null, | ||||
| 				null, | ||||
| 				null, | ||||
| 				0x66, | ||||
| 				0x74, | ||||
| 				0x79, | ||||
| 				0x70, | ||||
| 				0x6d, | ||||
| 				0x70, | ||||
| 				0x34, | ||||
| 				0x32, | ||||
| 			]) | ||||
| 		) { | ||||
| 			return '.mp3'; | ||||
| 		} else if ( | ||||
| 			startsWith(data, [ | ||||
| 				null, | ||||
| 				null, | ||||
| 				null, | ||||
| 				null, | ||||
| 				0x66, | ||||
| 				0x74, | ||||
| 				0x79, | ||||
| 				0x70, | ||||
| 				0x69, | ||||
| 				0x73, | ||||
| 				0x6f, | ||||
| 				0x6d, | ||||
| 			]) || | ||||
| 			startsWith(data, [ | ||||
| 				null, | ||||
| 				null, | ||||
| 				null, | ||||
| 				null, | ||||
| 				0x66, | ||||
| 				0x74, | ||||
| 				0x79, | ||||
| 				0x70, | ||||
| 				0x6d, | ||||
| 				0x70, | ||||
| 				0x34, | ||||
| 				0x32, | ||||
| 			]) | ||||
| 		) { | ||||
| 			return '.mp4'; | ||||
| 		} else { | ||||
| 			return '.bin'; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async export(id) { | ||||
| 		let all_messages = ''; | ||||
| 		let sequence = -1; | ||||
| 		let messages_done = 0; | ||||
| 		let messages_max = ( | ||||
| 			await tfrpc.rpc.query( | ||||
| 				'SELECT MAX(sequence) AS total FROM messages WHERE author = ?', | ||||
| 				[id] | ||||
| 			) | ||||
| 		)[0].total; | ||||
| 		while (true) { | ||||
| 			let messages = await tfrpc.rpc.query( | ||||
| 				` | ||||
| 				SELECT author, id, sequence, timestamp, hash, json(content) AS content, signature, flags | ||||
| 				FROM messages | ||||
| 				WHERE author = ? AND SEQUENCE > ? | ||||
| 				ORDER BY sequence LIMIT 100 | ||||
| 				`, | ||||
| 				[id, sequence] | ||||
| 			); | ||||
| 			if (messages?.length) { | ||||
| 				all_messages += | ||||
| 					messages | ||||
| 						.map((x) => JSON.stringify(this.format_message(x))) | ||||
| 						.join('\n') + '\n'; | ||||
| 				sequence = messages[messages.length - 1].sequence; | ||||
| 				messages_done += messages.length; | ||||
| 				this.progress = { | ||||
| 					name: 'messages', | ||||
| 					value: messages_done, | ||||
| 					max: messages_max, | ||||
| 				}; | ||||
| 			} else { | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		let zip = new JSZip(); | ||||
| 		zip.file(`message/classic/${this.sanitize(id)}.ndjson`, all_messages); | ||||
|  | ||||
| 		let blobs = await tfrpc.rpc.query( | ||||
| 			`SELECT messages_refs.ref AS id | ||||
| 			FROM messages | ||||
| 			JOIN messages_refs ON messages.id = messages_refs.message | ||||
| 			WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`, | ||||
| 			[id] | ||||
| 		); | ||||
| 		let blobs_done = 0; | ||||
| 		for (let row of blobs) { | ||||
| 			this.progress = {name: 'blobs', value: blobs_done, max: blobs.length}; | ||||
| 			let blob; | ||||
| 			try { | ||||
| 				blob = await tfrpc.rpc.get_blob(row.id); | ||||
| 			} catch (e) { | ||||
| 				console.log(`Failed to get ${row.id}: ${e.message}`); | ||||
| 			} | ||||
| 			if (blob) { | ||||
| 				zip.file( | ||||
| 					`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`, | ||||
| 					new Uint8Array(blob) | ||||
| 				); | ||||
| 			} | ||||
| 			blobs_done++; | ||||
| 		} | ||||
|  | ||||
| 		this.progress = {name: 'saving'}; | ||||
| 		let blob = await zip.generateAsync({type: 'blob'}); | ||||
| 		saveAs(blob, `${this.sanitize(id)}.zip`); | ||||
| 		this.progress = null; | ||||
| 	} | ||||
|  | ||||
| 	keypress(event) { | ||||
| 		if (event.key == 'Enter') { | ||||
| 			this.search(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async import(event) { | ||||
| 		let file = event.target.files[0]; | ||||
| 		if (!file) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		this.progress = {name: 'loading'}; | ||||
| 		let zip = new JSZip(); | ||||
| 		file = await zip.loadAsync(file); | ||||
| 		let messages = []; | ||||
| 		let blobs = []; | ||||
| 		file.forEach(function (path, entry) { | ||||
| 			if (!entry.dir) { | ||||
| 				if (path.startsWith('message/classic/')) { | ||||
| 					messages.push(entry); | ||||
| 				} else { | ||||
| 					blobs.push(entry); | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 		let success = {messages: 0, blobs: 0}; | ||||
| 		let progress = 0; | ||||
| 		let total_messages = 0; | ||||
| 		for (let entry of messages) { | ||||
| 			let lines = (await entry.async('string')).split('\n'); | ||||
| 			total_messages += lines.length; | ||||
| 			for (let line of lines) { | ||||
| 				if (!line.length) { | ||||
| 					continue; | ||||
| 				} | ||||
| 				let message = JSON.parse(line); | ||||
| 				this.progress = { | ||||
| 					name: 'messages', | ||||
| 					value: progress++, | ||||
| 					max: total_messages, | ||||
| 				}; | ||||
| 				if (await tfrpc.rpc.store_message(message.value)) { | ||||
| 					success.messages++; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		progress = 0; | ||||
| 		for (let blob of blobs) { | ||||
| 			this.progress = {name: 'blobs', value: progress++, max: blobs.length}; | ||||
| 			if (await tfrpc.rpc.store_blob(await blob.async('arraybuffer'))) { | ||||
| 				success.blobs++; | ||||
| 			} | ||||
| 		} | ||||
| 		this.progress = undefined; | ||||
| 		this.result = `imported ${success.messages} messages and ${success.blobs} blobs`; | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let progress; | ||||
| 		if (this.progress) { | ||||
| 			if (this.progress.max) { | ||||
| 				progress = html`<div> | ||||
| 					<label for="progress">${this.progress.name}</label | ||||
| 					><progress | ||||
| 						value=${this.progress.value} | ||||
| 						max=${this.progress.max} | ||||
| 					></progress> | ||||
| 				</div>`; | ||||
| 			} else { | ||||
| 				progress = html`<div><span>${this.progress.name}</span></div>`; | ||||
| 			} | ||||
| 		} | ||||
| 		return html`<h1>SSB 👟net</h1> | ||||
| 			<code>${this.result}</code> | ||||
| 			${progress} | ||||
|  | ||||
| 			<h2>Import</h2> | ||||
| 			<input type="file" id="import" @change=${this.import}></input> | ||||
|  | ||||
| 			<h2>Export</h2> | ||||
| 			<input type="text" id="search" @keypress=${this.keypress}></input> | ||||
| 			<input type="button" value="Search Users" @click=${this.search}></input> | ||||
| 			<ul> | ||||
| 				${Object.entries(this.feeds).map( | ||||
| 					([id, name]) => html` | ||||
| 						<li> | ||||
| 							${this.progress | ||||
| 								? undefined | ||||
| 								: html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`} | ||||
| 							${name} | ||||
| 							<code style="color: #ccc">${id}</code> | ||||
| 						</li> | ||||
| 					` | ||||
| 				)} | ||||
| 			</ul> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
| customElements.define('tf-sneaker-app', TfSneakerAppElement); | ||||
							
								
								
									
										5
									
								
								apps/ssb.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/ssb.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "🐌", | ||||
| 	"previous": "&sqnidhPsKb45FDw4m0/ZeDQgX3qxqw9MReaVP11Xk2M=.sha256" | ||||
| } | ||||
							
								
								
									
										123
									
								
								apps/ssb/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								apps/ssb/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| 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 getServerIdentity() { | ||||
| 	return ssb.getServerIdentity(); | ||||
| }); | ||||
| tfrpc.register(async function setServerFollowingMe(id, following) { | ||||
| 	return ssb.setServerFollowingMe(id, following); | ||||
| }); | ||||
| tfrpc.register(async function getIdentities() { | ||||
| 	return ssb.getIdentities(); | ||||
| }); | ||||
| tfrpc.register(async function getAllIdentities() { | ||||
| 	return ssb.getAllIdentities(); | ||||
| }); | ||||
| tfrpc.register(async function following(ids, depth) { | ||||
| 	return ssb.following(ids, depth); | ||||
| }); | ||||
| 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(async function store_message(message) { | ||||
| 	return await ssb.storeMessage(message); | ||||
| }); | ||||
| tfrpc.register(function apps() { | ||||
| 	return core.apps(); | ||||
| }); | ||||
| tfrpc.register(async function try_decrypt(id, content) { | ||||
| 	return await ssb.privateMessageDecrypt(id, content); | ||||
| }); | ||||
| tfrpc.register(async function encrypt(id, recipients, content) { | ||||
| 	return await ssb.privateMessageEncrypt(id, recipients, content); | ||||
| }); | ||||
| tfrpc.register(async function getActiveIdentity() { | ||||
| 	return await ssb.getActiveIdentity(); | ||||
| }); | ||||
| 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()); | ||||
| }); | ||||
| core.register('setActiveIdentity', async function (id) { | ||||
| 	await tfrpc.rpc.set('identity', id); | ||||
| }); | ||||
|  | ||||
| async function main() { | ||||
| 	if (typeof database !== 'undefined') { | ||||
| 		g_database = await database('ssb'); | ||||
| 	} | ||||
| 	await app.setDocument(utf8Decode(await getFile('index.html'))); | ||||
| } | ||||
| main(); | ||||
							
								
								
									
										94
									
								
								apps/ssb/commonmark-hashtag.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								apps/ssb/commonmark-hashtag.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| 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); | ||||
| 	if (link.startsWith('#')) { | ||||
| 		linkNode.destination = `#q=${encodeURIComponent(link)}`; | ||||
| 	} else { | ||||
| 		linkNode.destination = 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('(?:https?://[^ ]+[^ .,])|(?:(?<!\\w)#[\\w-]+)|(?:@[A-Za-z0-9+/]+=.ed25519)|(?:[%&][A-Za-z0-9+/]+=.sha256)'); | ||||
|  | ||||
| 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; | ||||
| } | ||||
							
								
								
									
										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
											
										
									
								
							
							
								
								
									
										165
									
								
								apps/ssb/emojis.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								apps/ssb/emojis.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| 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; | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| async function get_recent(author) { | ||||
| 	let recent = await tfrpc.rpc.query( | ||||
| 		` | ||||
| 		SELECT DISTINCT content ->> '$.vote.expression' AS value | ||||
| 		FROM messages | ||||
| 		WHERE author = ? AND | ||||
| 		content ->> '$.type' = 'vote' | ||||
| 		ORDER BY timestamp DESC LIMIT 10 | ||||
| 	`, | ||||
| 		[author] | ||||
| 	); | ||||
| 	return recent.map((x) => x.value); | ||||
| } | ||||
|  | ||||
| export async function picker(callback, anchor, author) { | ||||
| 	let json = await get_emojis(); | ||||
| 	let recent = await get_recent(author); | ||||
|  | ||||
| 	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 chosen(event) { | ||||
| 		console.log(event.srcElement.innerText); | ||||
| 		callback(event.srcElement.innerText); | ||||
| 		cleanup(); | ||||
| 	} | ||||
|  | ||||
| 	function refresh() { | ||||
| 		while (list.firstChild) { | ||||
| 			list.removeChild(list.firstChild); | ||||
| 		} | ||||
| 		let search = input.value.toLowerCase(); | ||||
| 		let any_at_all = false; | ||||
| 		if (recent) { | ||||
| 			let emoji_to_name = {}; | ||||
| 			for (let row of Object.values(json)) { | ||||
| 				for (let entry of Object.entries(row)) { | ||||
| 					emoji_to_name[entry[1]] = entry[0]; | ||||
| 				} | ||||
| 			} | ||||
| 			let header = document.createElement('div'); | ||||
| 			header.appendChild(document.createTextNode('Recent')); | ||||
| 			list.appendChild(header); | ||||
| 			let any = false; | ||||
| 			for (let entry of recent) { | ||||
| 				if ( | ||||
| 					search && | ||||
| 					search.length && | ||||
| 					(emoji_to_name[entry] || '').toLowerCase().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 = chosen; | ||||
| 				emoji.title = emoji_to_name[entry] || entry; | ||||
| 				emoji.appendChild(document.createTextNode(entry)); | ||||
| 				list.appendChild(emoji); | ||||
| 				any = true; | ||||
| 			} | ||||
| 			if (!any) { | ||||
| 				list.removeChild(header); | ||||
| 			} | ||||
| 		} | ||||
| 		for (let row of Object.entries(json)) { | ||||
| 			let header = document.createElement('div'); | ||||
| 			header.appendChild(document.createTextNode(row[0])); | ||||
| 			list.appendChild(header); | ||||
| 			let any = false; | ||||
| 			for (let entry of Object.entries(row[1])) { | ||||
| 				if ( | ||||
| 					search && | ||||
| 					search.length && | ||||
| 					entry[0].toLowerCase().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 = chosen; | ||||
| 				emoji.title = entry[0]; | ||||
| 				emoji.appendChild(document.createTextNode(entry[1])); | ||||
| 				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); | ||||
| } | ||||
							
								
								
									
										1
									
								
								apps/ssb/emojis.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/ssb/emojis.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										3
									
								
								apps/ssb/filesaver.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								apps/ssb/filesaver.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| (function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)}); | ||||
|  | ||||
| //# sourceMappingURL=FileSaver.min.js.map | ||||
							
								
								
									
										1
									
								
								apps/ssb/filesaver.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/ssb/filesaver.min.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										24
									
								
								apps/ssb/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								apps/ssb/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| <!doctype html> | ||||
| <html> | ||||
| 	<head> | ||||
| 		<title>Tilde Friends</title> | ||||
| 		<base target="_top" /> | ||||
| 		<link rel="stylesheet" href="tribute.css" /> | ||||
| 		<style> | ||||
| 			.tribute-container { | ||||
| 				color: #000; | ||||
| 			} | ||||
| 		</style> | ||||
| 	</head> | ||||
| 	<body style="margin: 0; padding: 0"> | ||||
| 		<tf-app></tf-app> | ||||
| 		<tf-reactions-modal id="reactions_modal"></tf-reactions-modal> | ||||
| 		<script> | ||||
| 			window.litDisableBundleWarning = true; | ||||
| 		</script> | ||||
| 		<script src="filesaver.min.js"></script> | ||||
| 		<script src="commonmark.min.js"></script> | ||||
| 		<script src="commonmark-hashtag.js" type="module"></script> | ||||
| 		<script src="script.js" type="module"></script> | ||||
| 	</body> | ||||
| </html> | ||||
							
								
								
									
										120
									
								
								apps/ssb/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								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
											
										
									
								
							
							
								
								
									
										17
									
								
								apps/ssb/script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								apps/ssb/script.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import {LitElement, html} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.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_reactions_modal from './tf-reactions-modal.js'; | ||||
| import * as tf_tab_mentions from './tf-tab-mentions.js'; | ||||
| import * as tf_tab_news from './tf-tab-news.js'; | ||||
| import * as tf_tab_news_feed from './tf-tab-news-feed.js'; | ||||
| import * as tf_tab_search from './tf-tab-search.js'; | ||||
| import * as tf_tab_connections from './tf-tab-connections.js'; | ||||
| import * as tf_tab_query from './tf-tab-query.js'; | ||||
| import * as tf_tag from './tf-tag.js'; | ||||
							
								
								
									
										384
									
								
								apps/ssb/tf-app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										384
									
								
								apps/ssb/tf-app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,384 @@ | ||||
| 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}, | ||||
| 			tags: {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; | ||||
| 		this.tags = []; | ||||
| 		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; | ||||
| 			} else if (name === 'identity') { | ||||
| 				self.whoami = value; | ||||
| 			} | ||||
| 		}); | ||||
| 		this.initial_load(); | ||||
| 	} | ||||
|  | ||||
| 	async initial_load() { | ||||
| 		let whoami = await tfrpc.rpc.getActiveIdentity(); | ||||
| 		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 if (this.hash === '#mentions') { | ||||
| 			this.tab = 'mentions'; | ||||
| 		} else if (this.hash.startsWith('#sql=')) { | ||||
| 			this.tab = 'query'; | ||||
| 		} else { | ||||
| 			this.tab = 'news'; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	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.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature | ||||
| 				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.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature | ||||
| 				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.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature | ||||
| 				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]; | ||||
| 			this.unread = this.unread.slice(this.unread.length - 1024); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	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()) || []; | ||||
| 			if (this.ids && !this.whoami) { | ||||
| 				this.whoami = this.ids[0]; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async load_recent_tags() { | ||||
| 		let start = new Date(); | ||||
| 		this.tags = await tfrpc.rpc.query( | ||||
| 			` | ||||
| 			WITH | ||||
| 				recent AS (SELECT id, json(content) AS content FROM messages | ||||
| 					WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post' | ||||
| 					ORDER BY timestamp DESC LIMIT 1024), | ||||
| 				recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag | ||||
| 					FROM recent | ||||
| 					WHERE json_extract(content, '$.channel') IS NOT NULL), | ||||
| 				recent_mentions AS (SELECT recent.id, json_extract(mention.value, '$.link') AS tag | ||||
| 					FROM recent, json_each(recent.content, '$.mentions') AS mention | ||||
| 					WHERE json_valid(mention.value) AND tag LIKE '#%'), | ||||
| 				combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions), | ||||
| 				by_message AS (SELECT DISTINCT id, tag FROM combined) | ||||
| 			SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10 | ||||
| 		`, | ||||
| 			[new Date() - 7 * 24 * 60 * 60 * 1000] | ||||
| 		); | ||||
| 		console.log('tags took', (new Date() - start) / 1000.0, 'seconds'); | ||||
| 	} | ||||
|  | ||||
| 	async load() { | ||||
| 		let whoami = this.whoami; | ||||
| 		let tags = this.load_recent_tags(); | ||||
| 		let following = await tfrpc.rpc.following([whoami], 2); | ||||
| 		let users = {}; | ||||
| 		let by_count = []; | ||||
| 		for (let [id, v] of Object.entries(following)) { | ||||
| 			users[id] = { | ||||
| 				following: v.of, | ||||
| 				blocking: v.ob, | ||||
| 				followed: v.if, | ||||
| 				blocked: v.ib, | ||||
| 			}; | ||||
| 			by_count.push({count: v.of, id: id}); | ||||
| 		} | ||||
| 		console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20)); | ||||
| 		let start_time = new Date(); | ||||
| 		users = await this.fetch_about(Object.keys(following).sort(), users); | ||||
| 		console.log( | ||||
| 			'about took', | ||||
| 			(new Date() - start_time) / 1000.0, | ||||
| 			'seconds for', | ||||
| 			Object.keys(users).length, | ||||
| 			'users' | ||||
| 		); | ||||
| 		this.following = Object.keys(following); | ||||
| 		this.users = users; | ||||
| 		await tags; | ||||
| 		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 | ||||
| 					id="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 === 'mentions') { | ||||
| 			return html` | ||||
| 				<tf-tab-mentions | ||||
| 					.following=${this.following} | ||||
| 					whoami=${this.whoami} | ||||
| 					.users="${this.users}}" | ||||
| 				></tf-tab-mentions> | ||||
| 			`; | ||||
| 		} 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> | ||||
| 			`; | ||||
| 		} else if (this.tab === 'query') { | ||||
| 			return html` | ||||
| 				<tf-tab-query | ||||
| 					.following=${this.following} | ||||
| 					whoami=${this.whoami} | ||||
| 					.users=${this.users} | ||||
| 					query=${this.hash?.startsWith('#sql=') | ||||
| 						? decodeURIComponent(this.hash.substring(5)) | ||||
| 						: null} | ||||
| 				></tf-tab-query> | ||||
| 			`; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async set_tab(tab) { | ||||
| 		this.tab = tab; | ||||
| 		if (tab === 'news') { | ||||
| 			await tfrpc.rpc.setHash('#'); | ||||
| 		} else if (tab === 'connections') { | ||||
| 			await tfrpc.rpc.setHash('#connections'); | ||||
| 		} else if (tab === 'mentions') { | ||||
| 			await tfrpc.rpc.setHash('#mentions'); | ||||
| 		} else if (tab === 'query') { | ||||
| 			await tfrpc.rpc.setHash('#sql='); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let self = this; | ||||
|  | ||||
| 		if (!this.loading && this.whoami && this.loaded !== this.whoami) { | ||||
| 			this.loading = true; | ||||
| 			this.load().finally(function () { | ||||
| 				self.loading = false; | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		const k_tabs = { | ||||
| 			'📰': 'news', | ||||
| 			'📡': 'connections', | ||||
| 			'@': 'mentions', | ||||
| 			'🔍': 'search', | ||||
| 			'👩💻': 'query', | ||||
| 		}; | ||||
|  | ||||
| 		let tabs = html` | ||||
| 			<div class="w3-bar w3-theme-l1"> | ||||
| 				${Object.entries(k_tabs).map( | ||||
| 					([k, v]) => html` | ||||
| 						<button | ||||
| 							title=${v} | ||||
| 							class="w3-bar-item w3-padding-large w3-hover-theme tab ${self.tab == | ||||
| 							v | ||||
| 								? 'w3-theme-l2' | ||||
| 								: 'w3-theme-l1'}" | ||||
| 							@click=${() => self.set_tab(v)} | ||||
| 						> | ||||
| 							${k} | ||||
| 						</button> | ||||
| 					` | ||||
| 				)} | ||||
| 			</div> | ||||
| 		`; | ||||
| 		let contents = !this.loaded | ||||
| 			? this.loading | ||||
| 				? html`<div class="w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge"> | ||||
| 					Loading... | ||||
| 				</div> | ||||
| 				${this.render_tab()}` | ||||
| 				: html`<div>Select or create an identity.</div>` | ||||
| 			: this.render_tab(); | ||||
| 		return html` | ||||
| 			<div | ||||
| 				style="width: 100vw; min-height: 100vh; height: 100%" | ||||
| 				class="w3-theme-dark" | ||||
| 			> | ||||
| 				${tabs} | ||||
| 				<div style="padding: 8px"> | ||||
| 					${this.tags.map( | ||||
| 						(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>` | ||||
| 					)} | ||||
| 					${contents} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-app', TfElement); | ||||
							
								
								
									
										574
									
								
								apps/ssb/tf-compose.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										574
									
								
								apps/ssb/tf-compose.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,574 @@ | ||||
| import {LitElement, html, unsafeHTML, live} 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}, | ||||
| 			author: {type: String}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	static styles = styles; | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.users = {}; | ||||
| 		this.root = undefined; | ||||
| 		this.branch = undefined; | ||||
| 		this.apps = undefined; | ||||
| 		this.drafts = {}; | ||||
| 		this.author = undefined; | ||||
| 	} | ||||
|  | ||||
| 	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) { | ||||
| 			setTimeout(() => this.notify(draft), 0); | ||||
| 		} | ||||
| 		return tfutils.markdown(text); | ||||
| 	} | ||||
|  | ||||
| 	input(event) { | ||||
| 		let edit = this.renderRoot.getElementById('edit'); | ||||
| 		let preview = this.renderRoot.getElementById('preview'); | ||||
| 		preview.innerHTML = this.process_text(edit.innerText); | ||||
| 		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; | ||||
| 		} | ||||
| 		let draft = this.get_draft(); | ||||
| 		draft.text = edit.innerText; | ||||
| 		draft.content_warning = content_warning?.innerText; | ||||
| 		setTimeout(() => this.notify(draft), 0); | ||||
| 	} | ||||
|  | ||||
| 	notify(draft) { | ||||
| 		this.dispatchEvent( | ||||
| 			new CustomEvent('tf-draft', { | ||||
| 				bubbles: true, | ||||
| 				composed: true, | ||||
| 				detail: { | ||||
| 					id: this.branch, | ||||
| 					draft: 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.innerText += `\n`; | ||||
| 			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; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async submit() { | ||||
| 		let self = this; | ||||
| 		let draft = this.get_draft(); | ||||
| 		let edit = this.renderRoot.getElementById('edit'); | ||||
| 		let message = { | ||||
| 			type: 'post', | ||||
| 			text: edit.innerText, | ||||
| 		}; | ||||
| 		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); | ||||
| 		if (draft.encrypt_to) { | ||||
| 			let to = new Set(draft.encrypt_to); | ||||
| 			to.add(this.whoami); | ||||
| 			to = [...to]; | ||||
| 			message.recps = to; | ||||
| 			console.log('message is now', message); | ||||
| 			message = await tfrpc.rpc.encrypt( | ||||
| 				this.whoami, | ||||
| 				to, | ||||
| 				JSON.stringify(message) | ||||
| 			); | ||||
| 			console.log('encrypted as', message); | ||||
| 		} | ||||
| 		try { | ||||
| 			await tfrpc.rpc.appendMessage(this.whoami, message).then(function () { | ||||
| 				edit.innerText = ''; | ||||
| 				self.input(); | ||||
| 				self.notify(undefined); | ||||
| 				self.requestUpdate(); | ||||
| 			}); | ||||
| 		} catch (error) { | ||||
| 			alert(error.message); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	discard() { | ||||
| 		this.notify(undefined); | ||||
| 	} | ||||
|  | ||||
| 	attach() { | ||||
| 		let self = this; | ||||
| 		let input = document.createElement('input'); | ||||
| 		input.type = 'file'; | ||||
| 		input.onchange = function (event) { | ||||
| 			let file = event.target.files[0]; | ||||
| 			self.add_file(file); | ||||
| 		}; | ||||
| 		input.click(); | ||||
| 	} | ||||
|  | ||||
| 	async autocomplete(text, callback) { | ||||
| 		this.last_autocomplete = text; | ||||
| 		let results = []; | ||||
| 		try { | ||||
| 			let rows = await tfrpc.rpc.query( | ||||
| 				` | ||||
| 				SELECT json(messages.content) FROM messages_fts(?) | ||||
| 				JOIN messages ON messages.rowid = messages_fts.rowid | ||||
| 				WHERE messages.content LIKE ? | ||||
| 				ORDER BY timestamp DESC LIMIT 10 | ||||
| 			`, | ||||
| 				['"' + text.replace('"', '""') + '"', `%%`] | ||||
| 			); | ||||
| 			for (let row of rows) { | ||||
| 				for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) { | ||||
| 					if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) { | ||||
| 						results.push({key: match[1], value: match[2]}); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} finally { | ||||
| 			if (this.last_autocomplete === text) { | ||||
| 				callback(results); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	firstUpdated() { | ||||
| 		let values = Object.entries(this.users).map((x) => ({ | ||||
| 			key: x[1].name ?? x[0], | ||||
| 			value: x[0], | ||||
| 		})); | ||||
| 		if (this.author) { | ||||
| 			values = [].concat( | ||||
| 				[ | ||||
| 					{ | ||||
| 						key: this.users[this.author]?.name, | ||||
| 						value: this.author, | ||||
| 					}, | ||||
| 				], | ||||
| 				values | ||||
| 			); | ||||
| 		} | ||||
| 		let tribute = new Tribute({ | ||||
| 			collection: [ | ||||
| 				{ | ||||
| 					values: values, | ||||
| 					selectTemplate: function (item) { | ||||
| 						return item ? `[@${item.original.key}](${item.original.value})` : undefined; | ||||
| 					}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					trigger: '&', | ||||
| 					values: this.autocomplete, | ||||
| 					selectTemplate: function (item) { | ||||
| 						return item ? `` : undefined; | ||||
| 					}, | ||||
| 				}, | ||||
| 			], | ||||
| 		}); | ||||
| 		tribute.attach(this.renderRoot.getElementById('edit')); | ||||
| 	} | ||||
|  | ||||
| 	updated() { | ||||
| 		super.updated(); | ||||
| 		let edit = this.renderRoot.getElementById('edit'); | ||||
| 		if (this.last_updated_text !== edit.innerText) { | ||||
| 			let preview = this.renderRoot.getElementById('preview'); | ||||
| 			preview.innerHTML = this.process_text(edit.innerText); | ||||
| 			this.last_updated_text = edit.innerText; | ||||
| 		} | ||||
| 		let encrypt = this.renderRoot.getElementById('encrypt_to'); | ||||
| 		if (encrypt) { | ||||
| 			let tribute = new Tribute({ | ||||
| 				values: Object.entries(this.users).map((x) => ({ | ||||
| 					key: x[1].name, | ||||
| 					value: x[0], | ||||
| 				})), | ||||
| 				selectTemplate: function (item) { | ||||
| 					return item.original.value; | ||||
| 				}, | ||||
| 			}); | ||||
| 			tribute.attach(encrypt); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	remove_mention(id) { | ||||
| 		let draft = this.get_draft(); | ||||
| 		delete draft.mentions[id]; | ||||
| 		setTimeout(() => this.notify(), 0); | ||||
| 	} | ||||
|  | ||||
| 	render_mention(mention) { | ||||
| 		let self = this; | ||||
| 		return html` <div style="display: flex; flex-direction: row"> | ||||
| 			<div style="align-self: center; margin: 0.5em"> | ||||
| 				<button | ||||
| 					class="w3-button w3-theme-d1" | ||||
| 					title="Remove ${mention.name} mention" | ||||
| 					@click=${() => self.remove_mention(mention.link)} | ||||
| 				> | ||||
| 					🚮 | ||||
| 				</button> | ||||
| 			</div> | ||||
| 			<div style="display: flex; flex-direction: column"> | ||||
| 				<h3>${mention.name}</h3> | ||||
| 				<div style="padding-left: 1em"> | ||||
| 					${Object.entries(mention) | ||||
| 						.filter((x) => x[0] != 'name') | ||||
| 						.map( | ||||
| 							(x) => | ||||
| 								html`<div> | ||||
| 									<span style="font-weight: bold">${x[0]}</span>: ${x[1]} | ||||
| 								</div>` | ||||
| 						)} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</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 class="w3-card-4 w3-margin w3-padding"> | ||||
| 					<select id="select" class="w3-select w3-theme-d1"> | ||||
| 						${Object.keys(self.apps).map( | ||||
| 							(app) => html`<option value=${app}>${app}</option>` | ||||
| 						)} | ||||
| 					</select> | ||||
| 					<button class="w3-button w3-theme-d1" @click=${attach_selected_app}> | ||||
| 						Attach | ||||
| 					</button> | ||||
| 					<button | ||||
| 						class="w3-button w3-theme-d1" | ||||
| 						@click=${() => (this.apps = null)} | ||||
| 					> | ||||
| 						Cancel | ||||
| 					</button> | ||||
| 				</div> | ||||
| 			`; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render_attach_app_button() { | ||||
| 		let self = this; | ||||
| 		async function attach_app() { | ||||
| 			self.apps = await tfrpc.rpc.apps(); | ||||
| 		} | ||||
| 		if (!this.apps) { | ||||
| 			return html`<button class="w3-button w3-theme-d1" @click=${attach_app}> | ||||
| 				Attach App | ||||
| 			</button>`; | ||||
| 		} else { | ||||
| 			return html`<button | ||||
| 				class="w3-button w3-theme-d1" | ||||
| 				@click=${() => (this.apps = null)} | ||||
| 			> | ||||
| 				Discard App | ||||
| 			</button>`; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	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 class="w3-container w3-padding"> | ||||
| 					<p> | ||||
| 						<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input> | ||||
| 						<label for="cw">CW</label> | ||||
| 					</p> | ||||
| 					<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${this.input} @change=${this.change} value=${draft.content_warning}></input> | ||||
| 				</div> | ||||
| 			`; | ||||
| 		} else { | ||||
| 			return html` | ||||
| 				<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning('')}></input> | ||||
| 				<label for="cw">CW</label> | ||||
| 			`; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	get_draft() { | ||||
| 		return this.drafts[this.branch || ''] || {}; | ||||
| 	} | ||||
|  | ||||
| 	update_encrypt(event) { | ||||
| 		let input = event.srcElement; | ||||
| 		let matches = input.value.match(/@.*?\.ed25519/g); | ||||
| 		if (matches) { | ||||
| 			let draft = this.get_draft(); | ||||
| 			let to = [...new Set(matches.concat(draft.encrypt_to))]; | ||||
| 			this.set_encrypt(to); | ||||
| 			input.value = ''; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render_encrypt() { | ||||
| 		let draft = this.get_draft(); | ||||
| 		if (draft.encrypt_to === undefined) { | ||||
| 			return; | ||||
| 		} | ||||
| 		return html` | ||||
| 			<div style="display: flex; flex-direction: row; width: 100%"> | ||||
| 				<label for="encrypt_to">🔐 To:</label> | ||||
| 				<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input> | ||||
| 				<button class="w3-button w3-theme-d1" @click=${() => this.set_encrypt(undefined)}>🚮</button> | ||||
| 			</div> | ||||
| 			<ul> | ||||
| 				${draft.encrypt_to.map( | ||||
| 					(x) => html` | ||||
| 					<li> | ||||
| 						<tf-user id=${x} .users=${this.users}></tf-user> | ||||
| 						<input type="button" class="w3-button w3-theme-d1" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input> | ||||
| 					</li>` | ||||
| 				)} | ||||
| 			</ul> | ||||
| 		`; | ||||
| 	} | ||||
|  | ||||
| 	set_encrypt(encrypt) { | ||||
| 		let draft = this.get_draft(); | ||||
| 		draft.encrypt_to = encrypt; | ||||
| 		this.notify(draft); | ||||
| 		this.requestUpdate(); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let self = this; | ||||
| 		let draft = self.get_draft(); | ||||
| 		let content_warning = | ||||
| 			draft.content_warning !== undefined | ||||
| 				? html`<div class="w3-panel w3-round-xlarge w3-theme-d2"> | ||||
| 						<p id="content_warning_preview">${draft.content_warning}</p> | ||||
| 					</div>` | ||||
| 				: undefined; | ||||
| 		let encrypt = | ||||
| 			draft.encrypt_to !== undefined | ||||
| 				? undefined | ||||
| 				: html`<button | ||||
| 						class="w3-button w3-theme-d1" | ||||
| 						@click=${() => this.set_encrypt([])} | ||||
| 					> | ||||
| 						🔐 | ||||
| 					</button>`; | ||||
| 		let result = html` | ||||
| 			<div | ||||
| 				class="w3-card-4 w3-theme-d4 w3-padding-small" | ||||
| 				style="box-sizing: border-box" | ||||
| 			> | ||||
| 				${this.render_encrypt()} | ||||
| 				<div class="w3-container w3-padding-small"> | ||||
| 					<div class="w3-half"> | ||||
| 						<span | ||||
| 							class="w3-input w3-theme-d1 w3-border" | ||||
| 							style="resize: vertical; width: 100%; overflow: hidden; white-space: pre-wrap" | ||||
| 							placeholder="Write a post here." | ||||
| 							id="edit" | ||||
| 							@input=${this.input} | ||||
| 							@paste=${this.paste} | ||||
| 							contenteditable | ||||
| 							.innerText=${live(draft.text ?? '')} | ||||
| 							></span> | ||||
| 					</div> | ||||
| 					<div class="w3-half w3-padding"> | ||||
| 						${content_warning} | ||||
| 						<div id="preview"></div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				${Object.values(draft.mentions || {}).map((x) => | ||||
| 					self.render_mention(x) | ||||
| 				)} | ||||
| 				${this.render_attach_app()} ${this.render_content_warning()} | ||||
| 				<button class="w3-button w3-theme-d1" id="submit" @click=${this.submit}> | ||||
| 					Submit | ||||
| 				</button> | ||||
| 				<button class="w3-button w3-theme-d1" @click=${this.attach}> | ||||
| 					Attach | ||||
| 				</button> | ||||
| 				${this.render_attach_app_button()} ${encrypt} | ||||
| 				<button class="w3-button w3-theme-d1" @click=${this.discard}> | ||||
| 					Discard | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		`; | ||||
| 		return result; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-compose', TfComposeElement); | ||||
							
								
								
									
										802
									
								
								apps/ssb/tf-message.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										802
									
								
								apps/ssb/tf-message.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,802 @@ | ||||
| import {LitElement, html, render, 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}, | ||||
| 			format: {type: String}, | ||||
| 			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.format = 'message'; | ||||
| 		this.expanded = {}; | ||||
| 	} | ||||
|  | ||||
| 	show_reply() { | ||||
| 		let event = new CustomEvent('tf-draft', { | ||||
| 			bubbles: true, | ||||
| 			composed: true, | ||||
| 			detail: { | ||||
| 				id: this.message?.id, | ||||
| 				draft: { | ||||
| 					encrypt_to: this.message?.decrypted?.recps, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}); | ||||
| 		this.dispatchEvent(event); | ||||
| 	} | ||||
|  | ||||
| 	discard_reply() { | ||||
| 		this.dispatchEvent( | ||||
| 			new CustomEvent('tf-draft', { | ||||
| 				bubbles: true, | ||||
| 				composed: true, | ||||
| 				detail: {id: this.id, draft: undefined}, | ||||
| 			}) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	show_reactions() { | ||||
| 		let modal = document.getElementById('reactions_modal'); | ||||
| 		modal.users = this.users; | ||||
| 		modal.votes = this.message?.votes || []; | ||||
| 	} | ||||
|  | ||||
| 	render_votes() { | ||||
| 		function normalize_expression(expression) { | ||||
| 			if (expression === 'Like' || !expression) { | ||||
| 				return '👍'; | ||||
| 			} else if (expression === 'Unlike') { | ||||
| 				return '👎'; | ||||
| 			} else if (expression === 'heart') { | ||||
| 				return '❤️'; | ||||
| 			} else { | ||||
| 				return expression; | ||||
| 			} | ||||
| 		} | ||||
| 		if (this.message?.votes?.length) { | ||||
| 			return html`<div class="w3-button" @click=${this.show_reactions}> | ||||
| 				${(this.message.votes || []).map( | ||||
| 					(vote) => html` | ||||
| 						<span | ||||
| 							title="${this.users[vote.author]?.name ?? vote.author} ${new Date( | ||||
| 								vote.timestamp | ||||
| 							)}" | ||||
| 						> | ||||
| 							${normalize_expression(vote.content.vote.expression)} | ||||
| 						</span> | ||||
| 					` | ||||
| 				)} | ||||
| 			</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; | ||||
| 		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), null, this.whoami); | ||||
| 	} | ||||
|  | ||||
| 	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="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`<button | ||||
| 					class="w3-button w3-theme-d1" | ||||
| 					@click=${() => self.set_expanded(true)} | ||||
| 				> | ||||
| 					+ ${this.total_child_messages(this.message) + ' More'} | ||||
| 				</button>`; | ||||
| 			} else { | ||||
| 				return html`<button | ||||
| 						class="w3-button w3-theme-d1" | ||||
| 						@click=${() => self.set_expanded(false)} | ||||
| 					> | ||||
| 						Collapse</button | ||||
| 					>${(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_channels() { | ||||
| 		let content = this.message?.content; | ||||
| 		if (this?.messsage?.decrypted?.type == 'post') { | ||||
| 			content = this.message.decrypted; | ||||
| 		} | ||||
| 		let channels = []; | ||||
| 		if (typeof content.channel === 'string') { | ||||
| 			channels.push(`#${content.channel}`); | ||||
| 		} | ||||
| 		if (Array.isArray(content.mentions)) { | ||||
| 			for (let mention of content.mentions) { | ||||
| 				if (typeof mention?.link === 'string' && mention.link.startsWith('#')) { | ||||
| 					channels.push(mention.link); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let content = this.message?.content; | ||||
| 		if (this.message?.decrypted?.type == 'post') { | ||||
| 			content = this.message.decrypted; | ||||
| 		} | ||||
| 		let class_background = this.message?.decrypted | ||||
| 			? 'w3-pale-red' | ||||
| 			: 'w3-theme-d4'; | ||||
| 		let self = this; | ||||
| 		let raw_button; | ||||
| 		switch (this.format) { | ||||
| 			case 'raw': | ||||
| 				if (content?.type == 'post' || content?.type == 'blog') { | ||||
| 					raw_button = html`<button | ||||
| 						class="w3-button w3-theme-d1" | ||||
| 						@click=${() => (self.format = 'md')} | ||||
| 					> | ||||
| 						Markdown | ||||
| 					</button>`; | ||||
| 				} else { | ||||
| 					raw_button = html`<button | ||||
| 						class="w3-button w3-theme-d1" | ||||
| 						@click=${() => (self.format = 'message')} | ||||
| 					> | ||||
| 						Message | ||||
| 					</button>`; | ||||
| 				} | ||||
| 				break; | ||||
| 			case 'md': | ||||
| 				raw_button = html`<button | ||||
| 					class="w3-button w3-theme-d1" | ||||
| 					@click=${() => (self.format = 'message')} | ||||
| 				> | ||||
| 					Message | ||||
| 				</button>`; | ||||
| 				break; | ||||
| 			case 'decrypted': | ||||
| 				raw_button = html`<button | ||||
| 					class="w3-button w3-theme-d1" | ||||
| 					@click=${() => (self.format = 'raw')} | ||||
| 				> | ||||
| 					Raw | ||||
| 				</button>`; | ||||
| 				break; | ||||
| 			default: | ||||
| 				if (this.message.decrypted) { | ||||
| 					raw_button = html`<button | ||||
| 						class="w3-button w3-theme-d1" | ||||
| 						@click=${() => (self.format = 'decrypted')} | ||||
| 					> | ||||
| 						Decrypted | ||||
| 					</button>`; | ||||
| 				} else { | ||||
| 					raw_button = html`<button | ||||
| 						class="w3-button w3-theme-d1" | ||||
| 						@click=${() => (self.format = 'raw')} | ||||
| 					> | ||||
| 						Raw | ||||
| 					</button>`; | ||||
| 				} | ||||
| 				break; | ||||
| 		} | ||||
| 		function small_frame(inner) { | ||||
| 			let body; | ||||
| 			return html` | ||||
| 				<div | ||||
| 					class="w3-card-4 w3-theme-d4 w3-border-theme" | ||||
| 					style="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.format == 'raw' ? self.render_raw() : inner} | ||||
| 					${self.render_votes()} | ||||
| 					${(self.message.child_messages || []).map( | ||||
| 						(x) => html` | ||||
| 							<tf-message | ||||
| 								.message=${x} | ||||
| 								whoami=${self.whoami} | ||||
| 								.users=${self.users} | ||||
| 								.drafts=${self.drafts} | ||||
| 								.expanded=${self.expanded} | ||||
| 							></tf-message> | ||||
| 						` | ||||
| 					)} | ||||
| 				</div> | ||||
| 			`; | ||||
| 		} | ||||
| 		if (this.message?.type === 'contact_group') { | ||||
| 			return html` <div | ||||
| 				class="w3-card-4 w3-theme-d4 w3-border-theme" | ||||
| 				style="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 | ||||
| 				class="w3-card-4 w3-theme-d4 w3-border-theme" | ||||
| 				style="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=${content.root || this.message.id} | ||||
| 									branch=${this.message.id} | ||||
| 									.drafts=${this.drafts} | ||||
| 									@tf-discard=${this.discard_reply} | ||||
| 									author=${this.message.author} | ||||
| 								></tf-compose> | ||||
| 							` | ||||
| 						: html` | ||||
| 								<button class="w3-button w3-theme-d1" @click=${this.show_reply}> | ||||
| 									Reply | ||||
| 								</button> | ||||
| 							`; | ||||
| 				let self = this; | ||||
| 				let body; | ||||
| 				switch (this.format) { | ||||
| 					case 'raw': | ||||
| 						body = this.render_raw(); | ||||
| 						break; | ||||
| 					case 'md': | ||||
| 						body = html`<code | ||||
| 							style="white-space: pre-wrap; overflow-wrap: anywhere" | ||||
| 							>${content.text}</code | ||||
| 						>`; | ||||
| 						break; | ||||
| 					case 'message': | ||||
| 						body = unsafeHTML(tfutils.markdown(content.text)); | ||||
| 						break; | ||||
| 					case 'decrypted': | ||||
| 						body = html`<pre | ||||
| 							style="white-space: pre-wrap; overflow-wrap: anywhere" | ||||
| 						> | ||||
| ${JSON.stringify(content, null, 2)}</pre | ||||
| 						>`; | ||||
| 						break; | ||||
| 				} | ||||
| 				let content_warning = html` | ||||
| 					<div | ||||
| 						class="w3-panel w3-round-xlarge w3-theme-l4" | ||||
| 						style="cursor: pointer" | ||||
| 						@click=${(x) => this.toggle_expanded(':cw')} | ||||
| 					> | ||||
| 						<p>${content.contentWarning}</p> | ||||
| 					</div> | ||||
| 				`; | ||||
| 				let content_html = html` | ||||
| 					${this.render_channels()} | ||||
| 					<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; | ||||
| 				let is_encrypted = this.message?.decrypted | ||||
| 					? html`<span style="align-self: center">🔓</span>` | ||||
| 					: undefined; | ||||
| 				return html` | ||||
| 					<style> | ||||
| 						code { | ||||
| 							white-space: pre-wrap; | ||||
| 							overflow-wrap: break-word; | ||||
| 						} | ||||
| 						div { | ||||
| 							overflow-wrap: anywhere; | ||||
| 						} | ||||
| 						img { | ||||
| 							max-width: 100%; | ||||
| 							height: auto; | ||||
| 							display: block; | ||||
| 						} | ||||
| 					</style> | ||||
| 					<div | ||||
| 						class="w3-card-4 ${class_background} w3-border-theme" | ||||
| 						style="margin-top: 8px; padding: 16px" | ||||
| 					> | ||||
| 						<div style="display: flex; flex-direction: row"> | ||||
| 							<tf-user id=${this.message.author} .users=${this.users}></tf-user> | ||||
| 							${is_encrypted} | ||||
| 							<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()} | ||||
| 						<p> | ||||
| 							${reply} | ||||
| 							<button class="w3-button w3-theme-d1" @click=${this.react}> | ||||
| 								React | ||||
| 							</button> | ||||
| 						</p> | ||||
| 						${this.render_children()} | ||||
| 					</div> | ||||
| 				`; | ||||
| 			} else if (content.type === 'issue') { | ||||
| 				let is_encrypted = this.message?.decrypted | ||||
| 					? html`<span style="align-self: center">🔓</span>` | ||||
| 					: undefined; | ||||
| 				return html` | ||||
| 					<style> | ||||
| 						code { | ||||
| 							white-space: pre-wrap; | ||||
| 							overflow-wrap: break-word; | ||||
| 						} | ||||
| 						div { | ||||
| 							overflow-wrap: anywhere; | ||||
| 						} | ||||
| 						img { | ||||
| 							max-width: 100%; | ||||
| 							height: auto; | ||||
| 							display: block; | ||||
| 						} | ||||
| 					</style> | ||||
| 					<div | ||||
| 						class="w3-card-4 ${class_background} w3-border-theme" | ||||
| 						style="margin-top: 8px; padding: 16px" | ||||
| 					> | ||||
| 						<div style="display: flex; flex-direction: row"> | ||||
| 							<tf-user id=${this.message.author} .users=${this.users}></tf-user> | ||||
| 							${is_encrypted} | ||||
| 							<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> | ||||
| 						${content.text} ${this.render_votes()} | ||||
| 						<p> | ||||
| 							<button class="w3-button w3-theme-d1" @click=${this.react}> | ||||
| 								React | ||||
| 							</button> | ||||
| 						</p> | ||||
| 						${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; | ||||
| 				switch (this.format) { | ||||
| 					case 'raw': | ||||
| 						body = this.render_raw(); | ||||
| 						break; | ||||
| 					case 'md': | ||||
| 						body = content.summary; | ||||
| 						break; | ||||
| 					case 'message': | ||||
| 						body = 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} | ||||
| 						`; | ||||
| 						break; | ||||
| 				} | ||||
| 				let reply = | ||||
| 					this.drafts[this.message?.id] !== undefined | ||||
| 						? html` | ||||
| 								<tf-compose | ||||
| 									whoami=${this.whoami} | ||||
| 									.users=${this.users} | ||||
| 									root=${content.root || this.message.id} | ||||
| 									branch=${this.message.id} | ||||
| 									.drafts=${this.drafts} | ||||
| 									@tf-discard=${this.discard_reply} | ||||
| 									author=${this.message.author} | ||||
| 								></tf-compose> | ||||
| 							` | ||||
| 						: html` | ||||
| 								<button class="w3-button w3-theme-d1" @click=${this.show_reply}> | ||||
| 									Reply | ||||
| 								</button> | ||||
| 							`; | ||||
| 				return html` | ||||
| 					<style> | ||||
| 						code { | ||||
| 							white-space: pre-wrap; | ||||
| 							overflow-wrap: break-word; | ||||
| 						} | ||||
| 						div { | ||||
| 							overflow-wrap: anywhere; | ||||
| 						} | ||||
| 						img { | ||||
| 							max-width: 100%; | ||||
| 							height: auto; | ||||
| 							display: block; | ||||
| 						} | ||||
| 					</style> | ||||
| 					<div | ||||
| 						class="w3-card-4 w3-theme-d4 w3-border-theme" | ||||
| 						style="margin-top: 8px; padding: 16px" | ||||
| 					> | ||||
| 						<div style="display: flex; flex-direction: row"> | ||||
| 							<tf-user id=${this.message.author} .users=${this.users}></tf-user> | ||||
| 							<span style="flex: 1"></span> | ||||
| 							<span style="padding-right: 8px" | ||||
| 								><a target="_top" href=${'#' + self.message.id}>%</a> | ||||
| 								${new Date(this.message.timestamp).toLocaleString()}</span | ||||
| 							> | ||||
| 							<span>${raw_button}</span> | ||||
| 						</div> | ||||
|  | ||||
| 						<div>${body}</div> | ||||
| 						${this.render_mentions()} | ||||
| 						<div> | ||||
| 							${reply} | ||||
| 							<button class="w3-button w3-theme-d1" @click=${this.react}> | ||||
| 								React | ||||
| 							</button> | ||||
| 						</div> | ||||
| 						${this.render_votes()} ${this.render_children()} | ||||
| 					</div> | ||||
| 				`; | ||||
| 			} else if (content.type === 'pub') { | ||||
| 				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') { | ||||
| 				if (this.message?.decrypted) { | ||||
| 					if (this.format == 'decrypted') { | ||||
| 						return small_frame( | ||||
| 							html`<span>🔓</span> | ||||
| 								<pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>` | ||||
| 						); | ||||
| 					} else { | ||||
| 						return small_frame( | ||||
| 							html`<span>🔓</span> | ||||
| 								<div>${this.message.decrypted.type}</div>` | ||||
| 						); | ||||
| 					} | ||||
| 				} else { | ||||
| 					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); | ||||
							
								
								
									
										203
									
								
								apps/ssb/tf-news.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								apps/ssb/tf-news.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,203 @@ | ||||
| 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); | ||||
							
								
								
									
										312
									
								
								apps/ssb/tf-profile.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								apps/ssb/tf-profile.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,312 @@ | ||||
| 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}, | ||||
| 			server_follows_me: {type: Boolean}, | ||||
| 			following: {type: Boolean}, | ||||
| 			blocking: {type: Boolean}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	static styles = styles; | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		let self = this; | ||||
| 		this.editing = null; | ||||
| 		this.whoami = null; | ||||
| 		this.id = null; | ||||
| 		this.users = {}; | ||||
| 		this.size = 0; | ||||
| 		this.server_follows_me = undefined; | ||||
| 	} | ||||
|  | ||||
| 	async load() { | ||||
| 		if (this.whoami !== this._follow_whoami) { | ||||
| 			this._follow_whoami = this.whoami; | ||||
| 			this.following = undefined; | ||||
| 			this.blocking = undefined; | ||||
|  | ||||
| 			let result = await tfrpc.rpc.query( | ||||
| 				` | ||||
| 				SELECT json_extract(content, '$.following') AS following | ||||
| 				FROM messages WHERE author = ? AND | ||||
| 				json_extract(content, '$.type') = 'contact' AND | ||||
| 				json_extract(content, '$.contact') = ? AND | ||||
| 				following IS NOT NULL | ||||
| 				ORDER BY sequence DESC LIMIT 1 | ||||
| 			`, | ||||
| 				[this.whoami, this.id] | ||||
| 			); | ||||
| 			this.following = result?.[0]?.following ?? false; | ||||
| 			result = await tfrpc.rpc.query( | ||||
| 				` | ||||
| 				SELECT json_extract(content, '$.blocking') AS blocking | ||||
| 				FROM messages WHERE author = ? AND | ||||
| 				json_extract(content, '$.type') = 'contact' AND | ||||
| 				json_extract(content, '$.contact') = ? AND | ||||
| 				blocking IS NOT NULL | ||||
| 				ORDER BY sequence DESC LIMIT 1 | ||||
| 			`, | ||||
| 				[this.whoami, this.id] | ||||
| 			); | ||||
| 			this.blocking = result?.[0]?.blocking ?? false; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async initial_load() { | ||||
| 		this.server_follows_me = undefined; | ||||
| 		let server_id = await tfrpc.rpc.getServerIdentity(); | ||||
| 		let followed = await tfrpc.rpc.query( | ||||
| 			` | ||||
| 			SELECT json_extract(content, '$.following') AS following | ||||
| 			FROM messages | ||||
| 			WHERE author = ? AND | ||||
| 			json_extract(content, '$.type') = 'contact' AND | ||||
| 			json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1 | ||||
| 		`, | ||||
| 			[server_id, this.whoami] | ||||
| 		); | ||||
| 		let is_followed = false; | ||||
| 		for (let row of followed) { | ||||
| 			is_followed = row.following != 0; | ||||
| 		} | ||||
| 		this.server_follows_me = is_followed; | ||||
| 	} | ||||
|  | ||||
| 	modify(change) { | ||||
| 		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, | ||||
| 			publicWebHosting: original.publicWebHosting, | ||||
| 		}; | ||||
| 		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(); | ||||
| 	} | ||||
|  | ||||
| 	async server_follow_me(follow) { | ||||
| 		try { | ||||
| 			await tfrpc.rpc.setServerFollowingMe(this.whoami, follow); | ||||
| 		} catch (e) { | ||||
| 			console.log(e); | ||||
| 		} | ||||
| 		try { | ||||
| 			await this.initial_load(); | ||||
| 		} catch (e) { | ||||
| 			console.log(e); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		if ( | ||||
| 			this.id == this.whoami && | ||||
| 			this.editing && | ||||
| 			this.server_follows_me === undefined | ||||
| 		) { | ||||
| 			this.initial_load(); | ||||
| 		} | ||||
| 		this.load(); | ||||
| 		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) { | ||||
| 				let server_follow; | ||||
| 				if (this.server_follows_me === true) { | ||||
| 					server_follow = html`<button | ||||
| 						class="w3-button w3-theme-d1" | ||||
| 						@click=${() => this.server_follow_me(false)} | ||||
| 					> | ||||
| 						Server, Stop Following Me | ||||
| 					</button>`; | ||||
| 				} else if (this.server_follows_me === false) { | ||||
| 					server_follow = html`<button | ||||
| 						class="w3-button w3-theme-d1" | ||||
| 						@click=${() => this.server_follow_me(true)} | ||||
| 					> | ||||
| 						Server, Follow Me | ||||
| 					</button>`; | ||||
| 				} | ||||
| 				edit = html` | ||||
| 					<button class="w3-button w3-theme-d1" @click=${this.save_edits}> | ||||
| 						Save Profile | ||||
| 					</button> | ||||
| 					<button class="w3-button w3-theme-d1" @click=${this.discard_edits}> | ||||
| 						Discard | ||||
| 					</button> | ||||
| 					${server_follow} | ||||
| 				`; | ||||
| 			} else { | ||||
| 				edit = html`<button class="w3-button w3-theme-d1" @click=${this.edit}> | ||||
| 					Edit Profile | ||||
| 				</button>`; | ||||
| 			} | ||||
| 		} | ||||
| 		if (this.id !== this.whoami && this.following !== undefined) { | ||||
| 			follow = this.following | ||||
| 				? html`<button class="w3-button w3-theme-d1" @click=${this.unfollow}> | ||||
| 						Unfollow | ||||
| 					</button>` | ||||
| 				: html`<button class="w3-button w3-theme-d1" @click=${this.follow}> | ||||
| 						Follow | ||||
| 					</button>`; | ||||
| 		} | ||||
| 		if (this.id !== this.whoami && this.blocking !== undefined) { | ||||
| 			block = this.blocking | ||||
| 				? html`<button class="w3-button w3-theme-d1" @click=${this.unblock}> | ||||
| 						Unblock | ||||
| 					</button>` | ||||
| 				: html`<button class="w3-button w3-theme-d1" @click=${this.block}> | ||||
| 						Block | ||||
| 					</button>`; | ||||
| 		} | ||||
| 		let edit_profile = this.editing | ||||
| 			? html` | ||||
| 			<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px"> | ||||
| 				<div class="w3-container"> | ||||
| 					<div> | ||||
| 						<label for="name">Name:</label> | ||||
| 						<input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input> | ||||
| 					</div> | ||||
| 					<div><label for="description">Description:</label></div> | ||||
| 					<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea> | ||||
| 					<div> | ||||
| 						<label for="public_web_hosting">Public Web Hosting:</label> | ||||
| 						<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input> | ||||
| 					</div> | ||||
| 					<div> | ||||
| 						<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</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; gap: 1em"> | ||||
| 				${edit_profile} | ||||
| 				<div style="flex: 1 0 50%"> | ||||
| 					<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div> | ||||
| 					<div>${unsafeHTML(tfutils.markdown(description))}</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				Following ${profile.following} identities. | ||||
| 				Followed by ${profile.followed} identities. | ||||
| 				Blocking ${profile.blocking} identities. | ||||
| 				Blocked by ${profile.blocked} identities. | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				${edit} | ||||
| 				${follow} | ||||
| 				${block} | ||||
| 			</div> | ||||
| 		</div>`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-profile', TfProfileElement); | ||||
							
								
								
									
										68
									
								
								apps/ssb/tf-reactions-modal.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								apps/ssb/tf-reactions-modal.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| import {LitElement, html, unsafeHTML} from './lit-all.min.js'; | ||||
| import {styles} from './tf-styles.js'; | ||||
|  | ||||
| class TfReactionsModalElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			users: {type: Object}, | ||||
| 			votes: {type: Array}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	static styles = styles; | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.votes = []; | ||||
| 		this.users = {}; | ||||
| 	} | ||||
|  | ||||
| 	clear() { | ||||
| 		this.votes = []; | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let self = this; | ||||
| 		return this.votes?.length | ||||
| 			? html` <div | ||||
| 					class="w3-modal w3-animate-opacity" | ||||
| 					style="display: block; box-sizing: border-box" | ||||
| 				> | ||||
| 					<div class="w3-modal-content w3-card-4 w3-theme-d1"> | ||||
| 						<div class="w3-container w3-padding"> | ||||
| 							<header class="w3-container"> | ||||
| 								<h2>Reactions</h2> | ||||
| 								<span class="w3-button w3-display-topright" @click=${this.clear} | ||||
| 									>×</span | ||||
| 								> | ||||
| 							</header> | ||||
| 							<ul class="w3-theme-dark w3-container w3-ul"> | ||||
| 								${this.votes.map( | ||||
| 									(x) => html` | ||||
| 										<li class="w3-bar"> | ||||
| 											<span class="w3-bar-item" | ||||
| 												>${x?.content?.vote?.expression}</span | ||||
| 											> | ||||
| 											<tf-user | ||||
| 												class="w3-bar-item" | ||||
| 												id=${x.author} | ||||
| 												.users=${this.users} | ||||
| 											></tf-user> | ||||
| 											<span class="w3-bar-item w3-right" | ||||
| 												>${new Date(x?.timestamp).toLocaleString()}</span | ||||
| 											> | ||||
| 										</li> | ||||
| 									` | ||||
| 								)} | ||||
| 							</ul> | ||||
| 							<footer class="w3-container w3-padding"> | ||||
| 								<button class="w3-button" @click=${this.clear}>Close</button> | ||||
| 							</footer> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div>` | ||||
| 			: undefined; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-reactions-modal', TfReactionsModalElement); | ||||
							
								
								
									
										314
									
								
								apps/ssb/tf-styles.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										314
									
								
								apps/ssb/tf-styles.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,314 @@ | ||||
| import {css} from './lit-all.min.js'; | ||||
|  | ||||
| const tf = css` | ||||
| 	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: ' ±'; | ||||
| 	} | ||||
|  | ||||
| 	pre code { | ||||
| 		display: block; | ||||
| 		padding: 8px; | ||||
| 	} | ||||
|  | ||||
| 	blockquote { | ||||
| 		border-left: 4px solid #fff; | ||||
| 		padding: 8px; | ||||
| 		padding-left: 12px; | ||||
| 	} | ||||
| `; | ||||
|  | ||||
| // prettier-ignore | ||||
| const w3 = css` | ||||
| /* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */ | ||||
| html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit} | ||||
| /* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ | ||||
| html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} | ||||
| article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item} | ||||
| audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline} | ||||
| audio:not([controls]){display:none;height:0}[hidden],template{display:none} | ||||
| a{background-color:transparent}a:active,a:hover{outline-width:0} | ||||
| abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted} | ||||
| b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000} | ||||
| small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} | ||||
| sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none} | ||||
| code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible} | ||||
| button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold} | ||||
| button,input{overflow:visible}button,select{text-transform:none} | ||||
| button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button} | ||||
| button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0} | ||||
| button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText} | ||||
| fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em} | ||||
| legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto} | ||||
| [type=checkbox],[type=radio]{padding:0} | ||||
| [type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto} | ||||
| [type=search]{-webkit-appearance:textfield;outline-offset:-2px} | ||||
| [type=search]::-webkit-search-decoration{-webkit-appearance:none} | ||||
| ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit} | ||||
| /* End extract */ | ||||
| html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden} | ||||
| h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px} | ||||
| .w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace} | ||||
| h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px} | ||||
| hr{border:0;border-top:1px solid #eee;margin:20px 0} | ||||
| .w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit} | ||||
| .w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc} | ||||
| .w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1} | ||||
| .w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1} | ||||
| .w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center} | ||||
| .w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top} | ||||
| .w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px} | ||||
| .w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap} | ||||
| .w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)} | ||||
| .w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} | ||||
| .w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none} | ||||
| .w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none} | ||||
| .w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%} | ||||
| .w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none} | ||||
| .w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block} | ||||
| .w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s} | ||||
| .w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%} | ||||
| .w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc} | ||||
| .w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer} | ||||
| .w3-dropdown-hover:hover .w3-dropdown-content{display:block} | ||||
| .w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000} | ||||
| .w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000} | ||||
| .w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1} | ||||
| .w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px} | ||||
| .w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto} | ||||
| .w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%} | ||||
| .w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%} | ||||
| .w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px} | ||||
| .w3-main,#main{transition:margin-left .4s} | ||||
| .w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)} | ||||
| .w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px} | ||||
| .w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto} | ||||
| .w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0} | ||||
| .w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left} | ||||
| .w3-bar .w3-button{white-space:normal} | ||||
| .w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0} | ||||
| .w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%} | ||||
| .w3-responsive{display:block;overflow-x:auto} | ||||
| .w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before, | ||||
| .w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both} | ||||
| .w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%} | ||||
| .w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%} | ||||
| .w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%} | ||||
| .w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%} | ||||
| @media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%} | ||||
| .w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%} | ||||
| .w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}} | ||||
| @media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%} | ||||
| .w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%} | ||||
| .w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}} | ||||
| .w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px} | ||||
| .w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px} | ||||
| .w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell} | ||||
| .w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom} | ||||
| .w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important} | ||||
| @media (max-width:1205px){.w3-auto{max-width:95%}} | ||||
| @media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px} | ||||
| .w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative} | ||||
| .w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center} | ||||
| .w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}} | ||||
| @media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}} | ||||
| @media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}} | ||||
| @media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}} | ||||
| @media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}} | ||||
| .w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0} | ||||
| .w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2} | ||||
| .w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0} | ||||
| .w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0} | ||||
| .w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)} | ||||
| .w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)} | ||||
| .w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)} | ||||
| .w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} | ||||
| .w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} | ||||
| .w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none} | ||||
| .w3-display-position{position:absolute} | ||||
| .w3-circle{border-radius:50%} | ||||
| .w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px} | ||||
| .w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px} | ||||
| .w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px} | ||||
| .w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px} | ||||
| .w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word} | ||||
| .w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%} | ||||
| .w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)} | ||||
| .w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)} | ||||
| .w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}} | ||||
| .w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} | ||||
| .w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}} | ||||
| .w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}} | ||||
| .w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}} | ||||
| .w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}} | ||||
| .w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}} | ||||
| .w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}} | ||||
| .w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important} | ||||
| .w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1} | ||||
| .w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75} | ||||
| .w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)} | ||||
| .w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)} | ||||
| .w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)} | ||||
| .w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important} | ||||
| .w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important} | ||||
| .w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important} | ||||
| .w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important} | ||||
| .w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important} | ||||
| .w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important} | ||||
| .w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important} | ||||
| .w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important} | ||||
| .w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important} | ||||
| .w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important} | ||||
| .w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important} | ||||
| .w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important} | ||||
| .w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important} | ||||
| .w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important} | ||||
| .w3-padding-64{padding-top:64px!important;padding-bottom:64px!important} | ||||
| .w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important} | ||||
| .w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important} | ||||
| .w3-left{float:left!important}.w3-right{float:right!important} | ||||
| .w3-button:hover{color:#000!important;background-color:#ccc!important} | ||||
| .w3-transparent,.w3-hover-none:hover{background-color:transparent!important} | ||||
| .w3-hover-none:hover{box-shadow:none!important} | ||||
| /* Colors */ | ||||
| .w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important} | ||||
| .w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important} | ||||
| .w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important} | ||||
| .w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important} | ||||
| .w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important} | ||||
| .w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important} | ||||
| .w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important} | ||||
| .w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important} | ||||
| .w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important} | ||||
| .w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important} | ||||
| .w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important} | ||||
| .w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important} | ||||
| .w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important} | ||||
| .w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important} | ||||
| .w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important} | ||||
| .w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important} | ||||
| .w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important} | ||||
| .w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important} | ||||
| .w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important} | ||||
| .w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important} | ||||
| .w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important} | ||||
| .w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important} | ||||
| .w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important} | ||||
| .w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important} | ||||
| .w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important} | ||||
| .w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important} | ||||
| .w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important} | ||||
| .w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important} | ||||
| .w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important} | ||||
| .w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important} | ||||
| .w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important} | ||||
| .w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important} | ||||
| .w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important} | ||||
| .w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important} | ||||
| .w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important} | ||||
| .w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important} | ||||
| .w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important} | ||||
| .w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important} | ||||
| .w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important} | ||||
| .w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important} | ||||
| .w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important} | ||||
| .w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important} | ||||
| .w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important} | ||||
| .w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important} | ||||
| .w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important} | ||||
| .w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important} | ||||
| .w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important} | ||||
| .w3-text-red,.w3-hover-text-red:hover{color:#f44336!important} | ||||
| .w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important} | ||||
| .w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important} | ||||
| .w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important} | ||||
| .w3-text-white,.w3-hover-text-white:hover{color:#fff!important} | ||||
| .w3-text-black,.w3-hover-text-black:hover{color:#000!important} | ||||
| .w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important} | ||||
| .w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important} | ||||
| .w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important} | ||||
| .w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important} | ||||
| .w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important} | ||||
| .w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important} | ||||
| .w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important} | ||||
| .w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important} | ||||
| .w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important} | ||||
| .w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important} | ||||
| .w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important} | ||||
| .w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important} | ||||
| .w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important} | ||||
| .w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important} | ||||
| .w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important} | ||||
| .w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important} | ||||
| .w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important} | ||||
| .w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important} | ||||
| .w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important} | ||||
| .w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important} | ||||
| .w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important} | ||||
| .w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important} | ||||
| .w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important} | ||||
| .w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important} | ||||
| .w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important} | ||||
| .w3-border-black,.w3-hover-border-black:hover{border-color:#000!important} | ||||
| .w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important} | ||||
| .w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important} | ||||
| .w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important} | ||||
| .w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important} | ||||
| .w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important} | ||||
| `; | ||||
|  | ||||
| // prettier-ignore | ||||
| const w3_2016_riverside = css` | ||||
| .w3-theme-l5 {color:#000 !important; background-color:#f4f6f9 !important} | ||||
| .w3-theme-l4 {color:#000 !important; background-color:#d9e1ec !important} | ||||
| .w3-theme-l3 {color:#000 !important; background-color:#b4c3d8 !important} | ||||
| .w3-theme-l2 {color:#fff !important; background-color:#8ea6c5 !important} | ||||
| .w3-theme-l1 {color:#fff !important; background-color:#6888b1 !important} | ||||
| .w3-theme-d1 {color:#fff !important; background-color:#456185 !important} | ||||
| .w3-theme-d2 {color:#fff !important; background-color:#3d5676 !important} | ||||
| .w3-theme-d3 {color:#fff !important; background-color:#354b68 !important} | ||||
| .w3-theme-d4 {color:#fff !important; background-color:#2e4059 !important} | ||||
| .w3-theme-d5 {color:#fff !important; background-color:#26364a !important} | ||||
|  | ||||
| .w3-theme-light {color:#000 !important; background-color:#f4f6f9 !important} | ||||
| .w3-theme-dark {color:#fff !important; background-color:#26364a !important} | ||||
| .w3-theme-action {color:#fff !important; background-color:#26364a !important} | ||||
|  | ||||
| .w3-theme {color:#fff !important; background-color:#4c6a92 !important} | ||||
| .w3-text-theme {color:#4c6a92 !important} | ||||
| .w3-border-theme {border-color:#4c6a92 !important} | ||||
|  | ||||
| .w3-hover-theme:hover {color:#fff !important; background-color:#4c6a92 !important} | ||||
| .w3-hover-text-theme:hover {color:#4c6a92 !important} | ||||
| .w3-hover-border-theme:hover {border-color:#4c6a92 !important} | ||||
| `; | ||||
|  | ||||
| export let styles = [tf, w3, w3_2016_riverside]; | ||||
							
								
								
									
										195
									
								
								apps/ssb/tf-tab-connections.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								apps/ssb/tf-tab-connections.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,195 @@ | ||||
| import {LitElement, html} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
| import {styles} from './tf-styles.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}, | ||||
| 			server_identity: {type: String}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	static styles = styles; | ||||
|  | ||||
| 	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 || []; | ||||
| 		}); | ||||
| 		tfrpc.rpc.getServerIdentity().then(function (identity) { | ||||
| 			self.server_identity = identity; | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	render_connection_summary(connection) { | ||||
| 		if (connection.address && connection.port) { | ||||
| 			return html`<div> | ||||
| 				<small>${connection.address}:${connection.port}</small> | ||||
| 			</div>`; | ||||
| 		} else if (connection.tunnel) { | ||||
| 			return html`<div>room peer</div>`; | ||||
| 		} 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) { | ||||
| 			let connections = this.connections.map((x) => x.id); | ||||
| 			return html`${peers | ||||
| 				.filter((x) => connections.indexOf(x.pubkey) == -1) | ||||
| 				.map((x) => html`${self.render_room_peer(x)}`)}`; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async _tunnel(portal, target) { | ||||
| 		return tfrpc.rpc.createTunnel(portal, target); | ||||
| 	} | ||||
|  | ||||
| 	render_room_peer(connection) { | ||||
| 		let self = this; | ||||
| 		return html` | ||||
| 			<li> | ||||
| 				<button | ||||
| 					class="w3-button w3-theme-d1" | ||||
| 					@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)} | ||||
| 				> | ||||
| 					Connect | ||||
| 				</button> | ||||
| 				<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡 | ||||
| 			</li> | ||||
| 		`; | ||||
| 	} | ||||
|  | ||||
| 	render_broadcast(connection) { | ||||
| 		return html` | ||||
| 			<li class="w3-bar" style="overflow: hidden; overflow-wrap: nowrap"> | ||||
| 				<button | ||||
| 					class="w3-bar-item w3-button w3-theme-d1" | ||||
| 					@click=${() => tfrpc.rpc.connect(connection)} | ||||
| 				> | ||||
| 					Connect | ||||
| 				</button> | ||||
| 				<div class="w3-bar-item"> | ||||
| 					<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> | ||||
| 					${this.render_connection_summary(connection)} | ||||
| 				</div> | ||||
| 			</li> | ||||
| 		`; | ||||
| 	} | ||||
|  | ||||
| 	async forget_stored_connection(connection) { | ||||
| 		await tfrpc.rpc.forgetStoredConnection(connection); | ||||
| 		this.stored_connections = (await tfrpc.rpc.getStoredConnections()) || []; | ||||
| 	} | ||||
|  | ||||
| 	render_connection(connection) { | ||||
| 		return html` | ||||
| 			<button | ||||
| 				class="w3-button w3-theme-d1" | ||||
| 				@click=${() => tfrpc.rpc.closeConnection(connection.id)} | ||||
| 			> | ||||
| 				Close | ||||
| 			</button> | ||||
| 			<tf-user id=${connection.id} .users=${this.users}></tf-user> | ||||
| 			${connection.tunnel !== undefined | ||||
| 				? '🚇' | ||||
| 				: html`(${connection.host}:${connection.port})`} | ||||
| 			<div>${connection.requests.map(x => html` | ||||
| 				<span class="w3-tag w3-small">${x.request_number > 0 ? '🟩' : '🟥'} ${x.name}</span> | ||||
| 			`)}</div> | ||||
| 			<ul> | ||||
| 				${this.connections | ||||
| 					.filter((x) => x.tunnel === this.connections.indexOf(connection)) | ||||
| 					.map((x) => html`<li>${this.render_connection(x)}</li>`)} | ||||
| 				${this.render_room_peers(connection.id)} | ||||
| 			</ul> | ||||
| 		`; | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let self = this; | ||||
| 		return html` | ||||
| 			<div class="w3-container" style="box-sizing: border-box"> | ||||
| 				<h2>New Connection</h2> | ||||
| 				<textarea class="w3-input w3-theme-d1" id="code"></textarea> | ||||
| 				<button | ||||
| 					class="w3-button w3-theme-d1" | ||||
| 					@click=${() => | ||||
| 						tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)} | ||||
| 				> | ||||
| 					Connect | ||||
| 				</button> | ||||
| 				<h2>Broadcasts</h2> | ||||
| 				<ul class="w3-ul w3-border"> | ||||
| 					${this.broadcasts | ||||
| 						.filter((x) => x.address) | ||||
| 						.map((x) => self.render_broadcast(x))} | ||||
| 				</ul> | ||||
| 				<h2>Connections</h2> | ||||
| 				<ul class="w3-ul w3-border"> | ||||
| 					${this.connections | ||||
| 						.filter((x) => x.tunnel === undefined) | ||||
| 						.map( | ||||
| 							(x) => html` | ||||
| 								<li class="w3-bar">${this.render_connection(x)}</li> | ||||
| 							` | ||||
| 						)} | ||||
| 				</ul> | ||||
| 				<h2>Stored Connections</h2> | ||||
| 				<ul class="w3-ul w3-border"> | ||||
| 					${this.stored_connections.map( | ||||
| 						(x) => html` | ||||
| 							<li class="w3-bar"> | ||||
| 								<button | ||||
| 									class="w3-bar-item w3-button w3-theme-d1" | ||||
| 									@click=${() => self.forget_stored_connection(x)} | ||||
| 								> | ||||
| 									Forget | ||||
| 								</button> | ||||
| 								<button | ||||
| 									class="w3-bar-item w3-button w3-theme-d1" | ||||
| 									@click=${() => tfrpc.rpc.connect(x)} | ||||
| 								> | ||||
| 									Connect | ||||
| 								</button> | ||||
| 								<div class="w3-bar-item"> | ||||
| 									<tf-user id=${x.pubkey} .users=${self.users}></tf-user> | ||||
| 									<div><small>${x.address}:${x.port}</small></div> | ||||
| 								</div> | ||||
| 							</li> | ||||
| 						` | ||||
| 					)} | ||||
| 				</ul> | ||||
| 				<h2>Local Accounts</h2> | ||||
| 				<ul class="w3-ul w3-border"> | ||||
| 					${this.identities.map( | ||||
| 						(x) => | ||||
| 							html`<li class="w3-bar"> | ||||
| 								<tf-user id=${x} .users=${this.users}></tf-user> | ||||
| 								${x == this.server_identity ? html`- 🖥local server` : undefined} | ||||
| 							</li>` | ||||
| 					)} | ||||
| 				</ul> | ||||
| 			</div> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-tab-connections', TfTabConnectionsElement); | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user