Compare commits
6730 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f576a9f2e9 | |||
| fed121b0aa | |||
| 3e37c74264 | |||
| 3762c20aad | |||
| c96f1ba22b | |||
| 43c81358b2 | |||
| 6316a6ae44 | |||
| 575b416856 | |||
| 7b7f8c1592 | |||
| 3907d1c28f | |||
| c629d2f60e | |||
| 43b453804b | |||
| d867affc40 | |||
| c36bfc821c | |||
| 7e784da00a | |||
| b79f469008 | |||
| cf33569a21 | |||
| fb0a0c66c8 | |||
| aac0023338 | |||
| e3873ddef5 | |||
| f0991348e2 | |||
| fa6708c27e | |||
| 4427201326 | |||
| 4a4241806e | |||
| 3824f65d15 | |||
| 3c17e4a6d6 | |||
| 75513d08de | |||
| 7cb3b40493 | |||
| ab89804c55 | |||
| ab6cf93c2b | |||
| 4c80762e22 | |||
| 1f7e80c68d | |||
| e91b879a69 | |||
| 14885ba7a2 | |||
| 0dda187d96 | |||
| 680d8cac4d | |||
| a7fd7fd539 | |||
| 300d8b026a | |||
| d5a15ac275 | |||
| bbb5294b3b | |||
| 7731579796 | |||
| 55ab38a097 | |||
| 5367ee18fb | |||
| 45db39ec88 | |||
| 32f55de383 | |||
| 7e8dfa56d0 | |||
| 32bb4b1fc4 | |||
| ae9bb6f27f | |||
| 08ab51eeac | |||
| aa130c88da | |||
| 9523978861 | |||
| 5112340040 | |||
| 6fb40d465e | |||
| 8d7eaa769a | |||
| 7a18991342 | |||
| f18c64db9e | |||
| 7c560b6daa | |||
| de2add5d5d | |||
| 24710ee2fc | |||
| 1fbfdaf221 | |||
| c4f7e4d5aa | |||
| 9a6dccb79b | |||
| 3935152d08 | |||
| 72f9a51c27 | |||
| efdda8425d | |||
| 685cab38b9 | |||
| 85a96c6467 | |||
| 2f832a9bfe | |||
| 1cb32c174b | |||
| b899fd6ccc | |||
| f4aecb317f | |||
| ee0264f77d | |||
| 9bf8b936d4 | |||
| 9f01c8d1fb | |||
| df5ab4fa91 | |||
| 39465b50cb | |||
| a745c67dec | |||
| 55bec4fbe9 | |||
| 3a40348860 | |||
| 98262853c7 | |||
| 4b3aac21db | |||
| 7521f82cac | |||
| 9b7628c103 | |||
| 7d2f4cfd42 | |||
| 5822730797 | |||
| fc946ab0fa | |||
| 9b843daf2f | |||
| ab588f0e51 | |||
| b43b4aa9f9 | |||
| d3ff7655f7 | |||
| a1ab0d42fe | |||
| b9ca3ceacd | |||
| eb8491c91b | |||
| 78db74dad8 | |||
| 4897bccdc9 | |||
| aaf508e309 | |||
| 8eeefc72e9 | |||
| 8e896c4da3 | |||
| 2c2686c910 | |||
| b20063f8a8 | |||
| 11cc2aca9d | |||
| 8f5162c40d | |||
| 2982bd79f6 | |||
| 96c35e2dd3 | |||
| cb5b2e1470 | |||
| 37d5e4a5e9 | |||
| bf30c15d77 | |||
| 8acc92770d | |||
| bfed6edf41 | |||
| 07189f0637 | |||
| aa94d5d95c | |||
| d73126ecb2 | |||
| 76797704ea | |||
| e35ede0370 | |||
| 518e16e6d5 | |||
| 012f6c56e6 | |||
| 8711499121 | |||
| b64dbdce74 | |||
| 2e27a4134c | |||
| 8412ccfa9b | |||
| c707c8632b | |||
| 2700f1d053 | |||
| 142c285063 | |||
| c0ff9756a8 | |||
| 64c6c477dc | |||
| c28939c250 | |||
| 05dd3c4967 | |||
| 8c72c5d0e6 | |||
| 93293750ce | |||
| 220f709b3a | |||
| 6c336ae470 | |||
| a4a50a4a5c | |||
| 169e865bb6 | |||
| 8803d2b7e2 | |||
| ab16adfb7d | |||
| f90adcab89 | |||
| 6090c7bbfc | |||
| 12253064d1 | |||
| b2120a0a13 | |||
| ad030bfc1f | |||
| 60d665e866 | |||
| 2fd48a607d | |||
| 2f540e31a5 | |||
| 7bd9626496 | |||
| 58a5742bd3 | |||
| d90f983438 | |||
| 81a6e48fc0 | |||
| db4a6fa9e1 | |||
| 457f063c67 | |||
| 01eee96b29 | |||
| 25afb7cb3c | |||
| c12932b2a6 | |||
| 7ac1d07cc4 | |||
| 66adc2d597 | |||
| a9516d047f | |||
| e81d84502b | |||
| 32b2c217c7 | |||
| 7da555c255 | |||
| 88348660eb | |||
| 81d884f899 | |||
| 2dccefb33a | |||
| af6915b4fc | |||
| 3786ea4ca2 | |||
| c7f6777e48 | |||
| ba371f7468 | |||
| 761facf98a | |||
| f78fed5ede | |||
| add3732450 | |||
| c6af997542 | |||
| e9e8e90a94 | |||
| f44510e65f | |||
| ba1f6ffc84 | |||
| 3e4f02b41e | |||
| af17fb27b8 | |||
| 72013341db | |||
| 6f445ca99a | |||
| e2af78d8d3 | |||
| 4721aa1d24 | |||
| 4d4d6e1411 | |||
| 67f5293d6c | |||
| 923ff4b282 | |||
| 49dd76b91e | |||
| 34cfa51104 | |||
| 685276f056 | |||
| 03a79dc6dd | |||
| 83a4881498 | |||
| e29ee105aa | |||
| 454eb7e627 | |||
| 62d77231af | |||
| 706b4d6054 | |||
| da69ca215b | |||
| 95a94cdbe3 | |||
| 6b5f4aa0a9 | |||
| dea3f52fe9 | |||
| a388fde3e2 | |||
| 1cde686a13 | |||
| 592f2931dc | |||
| 030fcb57a5 | |||
| 8be30acb11 | |||
| b86630f0e3 | |||
| 81c89f69c5 | |||
| 9b633251d5 | |||
| 70c6a4b567 | |||
| 5cd615c495 | |||
| 88f5ea675d | |||
| ac5fee0a69 | |||
| 274d6a9597 | |||
| d190cdc307 | |||
| b896111269 | |||
| 6137afeb28 | |||
| 2ebf33544f | |||
| 45f1991d4e | |||
| aa283a3c9e | |||
| 34ee566d88 | |||
| 3649cf46d3 | |||
| 8f00eb6771 | |||
| 01ec51891a | |||
| 738876a563 | |||
| 2d9b4b3896 | |||
| ba06e430c4 | |||
| ac08e52410 | |||
| 1bb82108b7 | |||
| e133005b44 | |||
| c0cb66233a | |||
| d82cdd3b19 | |||
| 6a51b02bed | |||
| 47d2c063fa | |||
| 540514c805 | |||
| db58a66e19 | |||
| 91a67bdac3 | |||
| 48d3fce22d | |||
| 9f4598638d | |||
| cde935629d | |||
| fbe81ad823 | |||
| c3d7a4977a | |||
| 9aab917836 | |||
| 54661cca95 | |||
| b58d09aa9a | |||
| 3b33237e51 | |||
| b8ec62e786 | |||
| 59763a84f8 | |||
| 1375a4849c | |||
| c0e3ad4b83 | |||
| 5305e373a0 | |||
| 877e3df71b | |||
| 286500e335 | |||
| 5937e6a6a8 | |||
| c6c22e394b | |||
| 378802a5ab | |||
| f963feab0f | |||
| d705a0ed9e | |||
| 7023fb1c99 | |||
| fe36dafcc7 | |||
| 04a6dbbedf | |||
| 29b427aab7 | |||
| 643b783dec | |||
| 872033a552 | |||
| 6d0f4e537e | |||
| 781fdf4fdc | |||
| dde4285cdf | |||
| 3322b47b6d | |||
| d457fd6db0 | |||
| 03f4700bd7 | |||
| b8321290f8 | |||
| 71b7521f42 | |||
| 106f7beb48 | |||
| d6f1c6cfdc | |||
| 4360ae7ff8 | |||
| 26cbe02a7f | |||
| 85b8d4f83a | |||
| bdc3da1fac | |||
| 95fcd5aa26 | |||
| 0ad83c43e4 | |||
| d6e4de4761 | |||
| f03a391f80 | |||
| e90f12ee32 | |||
| c541b3f1ce | |||
| 6192325fe0 | |||
| d0b964837f | |||
| 65316ffb5c | |||
| dadc19897c | |||
| bd2f1858f4 | |||
| e32b8a75ee | |||
| a6fe8797f0 | |||
| 29e54806a4 | |||
| d9f0704048 | |||
| 75674d961a | |||
| 779afbcb39 | |||
| a3f5ec1ba2 | |||
| 524322280b | |||
| 905a884f72 | |||
| 157635476b | |||
| 18943d6519 | |||
| 42b3b73551 | |||
| ee9eccb85a | |||
| 03a8d9edb6 | |||
| f1db4dc668 | |||
| 98c1710ac1 | |||
| 510833b2f2 | |||
| c2fdb4478d | |||
| 57d71ccd0f | |||
| d064d82fcc | |||
| 17f3920ddd | |||
| 9fc8048c30 | |||
| 9058dbf289 | |||
| e16e7bc098 | |||
| dbcd01bb43 | |||
| 40d1674a5c | |||
| 35a375e3d2 | |||
| 460f4f9254 | |||
| dbd6af745c | |||
| 2ac9448646 | |||
| 3141a7d7c1 | |||
| 1304e811d0 | |||
| 2ce1e7e6ef | |||
| 70efed1a58 | |||
| 9e4f109e80 | |||
| 5d54bf558c | |||
| fc5f0e8047 | |||
| 6bc584ba8b | |||
| ac4e504b8d | |||
| 0abee2a0bf | |||
| 4e4afdb795 | |||
| 9b429c1902 | |||
| b782dee2ef | |||
| 54e815085f | |||
| 0739da4ef4 | |||
| c300a6bdd6 | |||
| 901d53eb38 | |||
| a54471f737 | |||
| 1fae9cb3ef | |||
| 124bfc9328 | |||
| 53aa34fba5 | |||
| 58756a1973 | |||
| 7d9800b817 | |||
| e408590c21 | |||
| 735ccca18b | |||
| 946f47e037 | |||
| 4de1699c49 | |||
| 3d9221f054 | |||
| 1d1d59c757 | |||
| af43687354 | |||
| 2ec5acb55d | |||
| 2128f67dc3 | |||
| 55dda8420c | |||
| d595717e60 | |||
| 4603d4e578 | |||
| 12e525b664 | |||
| 1ac4cc4b11 | |||
| 7b218905fb | |||
| 080426dfdd | |||
| a89c1990d6 | |||
| 4e72290d53 | |||
| 7a7318b636 | |||
| 74d24f38f7 | |||
| 78dcae9143 | |||
| 0db640a679 | |||
| a3ddfd519b | |||
| e86d8861b9 | |||
| 2910e62bb6 | |||
| 2bb14698a5 | |||
| 2fd08d72dc | |||
| 222f62164c | |||
| bf8ee39a3f | |||
| cc1d1d76d0 | |||
| 30fdd96168 | |||
| cfad8d3614 | |||
| d9c3b880fc | |||
| 25c115739c | |||
| d97c514b8d | |||
| 6b822ccd61 | |||
| 47c5c4645e | |||
| ea0eaff212 | |||
| 41bf8c2d5f | |||
| d473a2e095 | |||
| d398ffd6df | |||
| ffab55452a | |||
| 9120b1dfd9 | |||
| 5d4e3183aa | |||
| 07171a95c4 | |||
| 12afcd3850 | |||
| 2d23330b74 | |||
| 7faff66006 | |||
| b07457726b | |||
| 6bf8142ff6 | |||
| cf0ccaf93d | |||
| 34a3955b60 | |||
| 82122872bf | |||
| 9c242a9ce6 | |||
| 51f3fac87b | |||
| 66b98844a2 | |||
| d03db00e4c | |||
| cf8d2bf6ef | |||
| 913a0b51a6 | |||
| ede0f696ee | |||
| d05214a169 | |||
| 1e93d0b19f | |||
| 5e125e646c | |||
| 1643201363 | |||
| 8e99e084a8 | |||
| 67d362fdd9 | |||
| e66430ada6 | |||
| 52ca60142a | |||
| 7d2a5afa6d | |||
| 02ba233644 | |||
| 50493a3330 | |||
| 8434f29c54 | |||
| a34426a7f6 | |||
| 033693283d | |||
| fab7b7f26a | |||
| 230e3b4ace | |||
| a50a627300 | |||
| 80930f6690 | |||
| 652b3a9208 | |||
| 0a8b825702 | |||
| f4e2a38f4b | |||
| b3474531c9 | |||
| f0597e0124 | |||
| 2fdc91bd04 | |||
| 16ca09eed8 | |||
| 129fb7f10f | |||
| 016e24472a | |||
| 4acb98c496 | |||
| 96d1b30012 | |||
| 6fc586598a | |||
| 2d9c938765 | |||
| 83d1a6c046 | |||
| 87393c7db8 | |||
| bd47667e63 | |||
| cfd865bf8b | |||
| 9b54df7b2b | |||
| 5da562fa6f | |||
| 83d952eae8 | |||
| f30be87879 | |||
| ab19480cc5 | |||
| 99451698a4 | |||
| 48dbed8001 | |||
| bae883a891 | |||
| 2aae2362e3 | |||
| f780e1dbc3 | |||
| fdc11b5e53 | |||
| 1a0bdb0f98 | |||
| e870bebd3d | |||
| dd0ca91aa9 | |||
| 36906e2ddb | |||
| 3eaed30446 | |||
| 72643026a3 | |||
| d6980cb8fa | |||
| dcd24fd516 | |||
| 75cc873d77 | |||
| dd23a1a401 | |||
| 6ac84a2465 | |||
| e4d3124e72 | |||
| df11a9e832 | |||
| e4703989fe | |||
| feb83ba161 | |||
| 22a98b3e4f | |||
| 934a0e8943 | |||
| 963c7690b6 | |||
| aeec4aa4a8 | |||
| 169b6b5572 | |||
| 6244d77d44 | |||
| f4839a3b4f | |||
| 4b21b67a45 | |||
| 799a83a115 | |||
| f3ebb2a314 | |||
| c0fbed914e | |||
| dd8c157bb9 | |||
| 8bd2d640f2 | |||
| 80aaa6c32b | |||
| 4e9fe8f53f | |||
| f926a2f777 | |||
| f8097221e6 | |||
| 95e7a76ba9 | |||
| 72bc13eb39 | |||
| 2206b80e65 | |||
| 39a8399977 | |||
| b7247fcedc | |||
| e4494707fa | |||
| 45b7cab203 | |||
| 4dfc3b9dbb | |||
| 15d05cd92f | |||
| ba1b1cc6fa | |||
| b716cde9b5 | |||
| 46a0645865 | |||
| 2b30f45f60 | |||
| 87b920698f | |||
| 4f4811e1d9 | |||
| db9936e07c | |||
| 7dcee2eb40 | |||
| 6b34ea6fe5 | |||
| b33b01df0f | |||
| 1c895432ed | |||
| ddd6a05198 | |||
| 0ccde7807f | |||
| 9f97992196 | |||
| 67b8da719f | |||
| 04fad564f7 | |||
| aee698c285 | |||
| 55b489b17a | |||
| c5d188c30a | |||
| 6fff3d6db5 | |||
| 8043d83921 | |||
| b2d83c1f80 | |||
| 1b91cd7037 | |||
| 0baf926c84 | |||
| 763e25e031 | |||
| 861023b3f6 | |||
| 1f5a83994b | |||
| af523522de | |||
| c3a266b3e7 | |||
| f41d815aa6 | |||
| ad8a93dde8 | |||
| b07d44a6c0 | |||
| 1dc899ba6e | |||
| 0b1e3edaff | |||
| 9fb4fed044 | |||
| 730b0b121d | |||
| 9d9d9e2cfa | |||
| 43bc09f392 | |||
| 195498e9db | |||
| 50332c4999 | |||
| 9e46646b16 | |||
| a74a03e414 | |||
| fe8b9eca8e | |||
| a7cc1ae630 | |||
| 14e008bbad | |||
| 6a12c265cf | |||
| c35cb57a79 | |||
| 51c776f51e | |||
| 7ce0eb26d4 | |||
| f0091fa811 | |||
| 25c88bc986 | |||
| ab06f57965 | |||
| c46209a159 | |||
| 0c47e27f9f | |||
| 0385f265e8 | |||
| 59b3960a42 | |||
| d5ab4ba2ef | |||
| f59b81f46b | |||
| 4c39ce8c96 | |||
| 94ff0ea4cd | |||
| 526758a07f | |||
| 9686fba81e | |||
| bbc547fd7f | |||
| 7d5dbeaf2d | |||
| 8c19cf45e2 | |||
| c9277de8ee | |||
| b5baca51a6 | |||
| fc800821f9 | |||
| efbc46937f | |||
| 9ceeb2d220 | |||
| 7b89862ed0 | |||
| 6804e4290a | |||
| 0a3d820fda | |||
| e2a9c95ea8 | |||
| 3aefc9f02e | |||
| 0042cb5292 | |||
| b288c97372 | |||
| a0de1740a4 | |||
| 8e2e3018d8 | |||
| 60e93024e7 | |||
| d89aa365f9 | |||
| 4241263f2c | |||
| b009f9c21b | |||
| 04ccec6de3 | |||
| e7b41fadd0 | |||
| 2dc1acef2e | |||
| b977b629f4 | |||
| 6bbd493e20 | |||
| 38ad435af5 | |||
| 8f245af317 | |||
| 9b2bfbe7d0 | |||
| f69ea85e55 | |||
| 7559a10526 | |||
| 68dbe959bb | |||
| 74a875aa23 | |||
| 4b679c5056 | |||
| cb8a260b9c | |||
| 7d7975b8bb | |||
| f12513da96 | |||
| 785a3e3381 | |||
| f5fcc20840 | |||
| 17e20c17b3 | |||
| 39fd350ffe | |||
| b4d100baff | |||
| 29bc83f4a7 | |||
| 13c8774d45 | |||
| c9212e770d | |||
| 08aa7803e3 | |||
| 770858c255 | |||
| c73bb7d408 | |||
| 15cd675354 | |||
| e8137d3f88 | |||
| c63abe9988 | |||
| a7a08fd760 | |||
| df51f95fbd | |||
| a14cf1a339 | |||
| 30758600f0 | |||
| 823242a93c | |||
| 482e3c4cb7 | |||
| 1dbd7158ad | |||
| 46b3f0babd | |||
| b4bc554d7a | |||
| 1485969493 | |||
| 92f7ddcf3e | |||
| 90d480b4cf | |||
| 2515d07c8f | |||
| 7acb9416c2 | |||
| 095e3a8a88 | |||
| 92623a246a | |||
| 0c4212ca90 | |||
| 9a06a05a75 | |||
| f05a0615f7 | |||
| 6b93687ff0 | |||
| 8d910d5e26 | |||
| 9264050f41 | |||
| eab0c54663 | |||
| 82a254b7bd | |||
| 0f6a59ed98 | |||
| afb4682987 | |||
| a18326519b | |||
| b033540f95 | |||
| ff529d733d | |||
| 5951958c05 | |||
| 886f2fb95d | |||
| f6ce0c581e | |||
| 5ce68b6c12 | |||
| c748aebfc5 | |||
| f8ec445ff0 | |||
| 5d4f347eaa | |||
| 1359c0574e | |||
| b1d239b292 | |||
| 70031415e8 | |||
| 3c97d17770 | |||
| 5a73a9758a | |||
| 6848e96244 | |||
| b53566e074 | |||
| 012b914a97 | |||
| 3c6eed0135 | |||
| f2a2e7f5dd | |||
| 15b6a9a257 | |||
| b83977e72f | |||
| 8f5082cd2f | |||
| 50c877cb60 | |||
| e3e7fd5900 | |||
| 69823f9cae | |||
| e5dcdf109a | |||
| 5c6a643436 | |||
| 1ca1a69eb8 | |||
| 76ea1ba192 | |||
| 8cd8ec134e | |||
| 6c649e2bfe | |||
| 9f42647a75 | |||
| 9747e5e0d5 | |||
| a4cacbc73a | |||
| f24840d09c | |||
| acb7991cc5 | |||
| 6addc7dd3d | |||
| 2bd60f4160 | |||
| e71362a6df | |||
| 673c09cd77 | |||
| 4606422e57 | |||
| 4bc8f968e0 | |||
| 35ee58dfe1 | |||
| 3705426b6d | |||
| 5813aea27f | |||
| 5ba836a31b | |||
| 4f387d15b9 | |||
| 8ab0246ca2 | |||
| ed68bd4108 | |||
| 3d39e8ec26 | |||
| 53bdab7cc8 | |||
| fafa55ba1d | |||
| 3a818c1419 | |||
| 422853298c | |||
| f3a2bd6b40 | |||
| 6834137f50 | |||
| c4d7fef0cd | |||
| 556b5ff628 | |||
| 2daa1ec4ce | |||
| 348ea72b70 | |||
| 4e14d810c5 | |||
| f84905b003 | |||
| 9bb87145f6 | |||
| c7675570aa | |||
| be942fd66a | |||
| 8544aca362 | |||
| c3308e1de9 | |||
| 047d7125a1 | |||
| cf4d3117c1 | |||
| 88c2844e48 | |||
| 6ca4694fe6 | |||
| 062287e270 | |||
| 50e0d76f10 | |||
| df039d3d8c | |||
| 799606b73c | |||
| c048f5d357 | |||
| f712a8cecb | |||
| e1d8d6bf4e | |||
| 14ed8a110c | |||
| cb6700f21b | |||
| d4f35bf07a | |||
| b4eb29138b | |||
| 8ca3a071f9 | |||
| 5abd211436 | |||
| 99f428e091 | |||
| a734fdf9b0 | |||
| 6f91b2bc80 | |||
| 32d7272939 | |||
| c8d85452bf | |||
| 557f800919 | |||
| 2119e88c78 | |||
| 24e7a7e6bf | |||
| 4e5847f356 | |||
| 72d580edfd | |||
| e878b7683b | |||
| fac700bf4d | |||
| 894c24880d | |||
| d742249fd1 | |||
| 9c100fea48 | |||
| 7daab62850 | |||
| fed0ced89d | |||
| df9cfaed1d | |||
| edd614dd0c | |||
| 375b228bb2 | |||
| 7143ef8a32 | |||
| 1bd7de5a18 | |||
| a1f73eee86 | |||
| d9f8710758 | |||
| 3a9d5439a2 | |||
| 5d1be6e8be | |||
| 775e85a2e8 | |||
| dc8f403c44 | |||
| cf9fa991d1 | |||
| d26dc49755 | |||
| be04559a66 | |||
| 0722dafe58 | |||
| ab906faa17 | |||
| 9d63a12469 | |||
| f40d0d24c7 | |||
| 2cdea36abf | |||
| 8d4f4c8832 | |||
| 3b4dcbb01d | |||
| b52f3b1c57 | |||
| 2cdf5629e5 | |||
| 97b4f0e369 | |||
| a508c8d236 | |||
| 7911e6371a | |||
| bbace01e9a | |||
| 87469fea63 | |||
| 48a9db0315 | |||
| 33c9471112 | |||
| 04fdcbb672 | |||
| 4d2286c5be | |||
| 620e45e4dc | |||
| fb97e74344 | |||
| 3bbb7df72d | |||
| 58b08821bf | |||
| 19bdfd5b84 | |||
| b6c857e2a2 | |||
| 0991626e85 | |||
| 7b10d4fac0 | |||
| 980dacaa87 | |||
| 3790e8abb4 | |||
| 14ba851daf | |||
| 95d80772d7 | |||
| 341e936082 | |||
| e87399c4a1 | |||
| a7678ea1e0 | |||
| 0388bd3b48 | |||
| f27327164b | |||
| a0f85075c1 | |||
| e97d18a03b | |||
| f07cdb43b1 | |||
| da8c8f166d | |||
| 0830a30498 | |||
| 68a2762b75 | |||
| 666e471369 | |||
| d89f4a74f9 | |||
| 191535d01a | |||
| 241203f804 | |||
| 26cc7a0f64 | |||
| 324f9e58ea | |||
| e71519c638 | |||
| 9b6cee0cab | |||
| 2a5f6a7b3d | |||
| cae03817cb | |||
| 37736184a0 | |||
| 9431a52abe | |||
| 13c664ad34 | |||
| d7640d9e15 | |||
| 3c36be9839 | |||
| 9dce7bbb97 | |||
| fa44300abc | |||
| 2783d162b7 | |||
| 39566dbd4f | |||
| f8186add92 | |||
| c15f22e81d | |||
| 052ac8f95b | |||
| c37f8ba4c7 | |||
| 7e2458d5b5 | |||
| fe989177bb | |||
| 412645f025 | |||
| e527c46f34 | |||
| da471b57ad | |||
| a4ff97c8df | |||
| 411fc47f28 | |||
| b97e3113d1 | |||
| f107d63fab | |||
| a65ac5a82b | |||
| c7c1db00e8 | |||
| 824f93daa3 | |||
| 9fec2f8c58 | |||
| 0b46389176 | |||
| db03f16b8e | |||
| f83fcb1a18 | |||
| ba0f1f86b9 | |||
| 81d349d993 | |||
| d65d2b94b9 | |||
| acd52bc070 | |||
| a64339763f | |||
| 4f21b95bbf | |||
| 03e4dcf1ed | |||
| 3c56d1372d | |||
| 27ecdef09b | |||
| 121e8a51c1 | |||
| 88e3ada701 | |||
| 92d822d494 | |||
| 62c93f54cc | |||
| 500c2ed4f2 | |||
| 425362edfe | |||
| 9c554f9b3d | |||
| b7a5f81f95 | |||
| 0746a64c83 | |||
| 22cd475d22 | |||
| 6904f6b36f | |||
| e25a649e1a | |||
| 0eb8cbee4d | |||
| 96e8f65af7 | |||
| 8fb036ba2d | |||
| 0a10fa12ef | |||
| e30dad7913 | |||
| c2a0c02898 | |||
| 7c1e89f86a | |||
| 29f8a4cba2 | |||
| b884accc99 | |||
| d56540320e | |||
| 526fe7e9a4 | |||
| aa696c4c15 | |||
| 3216d7e5a7 | |||
| b68f649414 | |||
| 543f35d9a8 | |||
| 03d42a98cd | |||
| 4f48554cd2 | |||
| 85d9c0f403 | |||
| 66b4279dd8 | |||
| 1ee84fbeca | |||
| c0e15b206a | |||
| 290575ba65 | |||
| bd8690de57 | |||
| 0d09f87777 | |||
| 0f45b7e516 | |||
| 4c552cc350 | |||
| 6a87f54292 | |||
| bfb2c5aad0 | |||
| db898e7bcf | |||
| dbebbe3a19 | |||
| 10e8aff46a | |||
| 81e2ea6a61 | |||
| 2594c59250 | |||
| b56a4ee6ec | |||
| ab524ad3b4 | |||
| 83bf80feec | |||
| b9e5417218 | |||
| 082169a756 | |||
| cdc87d371c | |||
| f8ec3bc591 | |||
| 85f4651e9b | |||
| da53241c48 | |||
| fa265296e8 | |||
| ccf296ea92 | |||
| d65b48204b | |||
| 977720cee4 | |||
| 3c7cdb1da8 | |||
| dd80e744ff | |||
| 0b12e37459 | |||
| 51e815f0cb | |||
| f3856d569d | |||
| e6d1909f0b | |||
| 408976a199 | |||
| 4da49d926b | |||
| 75750ed760 | |||
| 2f74779bf1 | |||
| 7b038393b1 | |||
| 768c0e7f77 | |||
| c6009b1056 | |||
| 2a280afa88 | |||
| be980f4bc9 | |||
| 78118e9442 | |||
| a2c10a7913 | |||
| 7c4f7c9dad | |||
| 62058e6d48 | |||
| a2f514b544 | |||
| 67434bc5d4 | |||
| 167183775e | |||
| 4ce6313126 | |||
| 1298ed61b6 | |||
| 14b424ee94 | |||
| 2bc1f2fe21 | |||
| b392dbf6f8 | |||
| 696b3ef1ce | |||
| 4e2ee3b3a8 | |||
| 1f9fab9a0c | |||
| 6170796ed8 | |||
| d91d1cea34 | |||
| 69ba32683c | |||
| fc78e63ad1 | |||
| db212a555d | |||
| 429f32dacb | |||
| ad036104e6 | |||
| 4607f0177c | |||
| d7dbaeba46 | |||
| 3e94db1837 | |||
| 4fd77c2f05 | |||
| 9fe05e7d40 | |||
| 946dcd0301 | |||
| 97d718e850 | |||
| 913b2d148c | |||
| 916f6a6126 | |||
| 7dbee19456 | |||
| 2375b00b52 | |||
| 13cdca028f | |||
| e5a67500e4 | |||
| 264636c879 | |||
| 9b50455049 | |||
| a531446396 | |||
| f876c35283 | |||
| 1bcec53c6b | |||
| 0ad5ef4e9b | |||
| 94726aa1d5 | |||
| 8da2f91e22 | |||
| b03555a50b | |||
| b61312feca | |||
| ea9459d330 | |||
| 3edfe70a7b | |||
| 5482a19979 | |||
| e1678aaf11 | |||
| 2bdb570f3c | |||
| 3723b2da39 | |||
| b63af00579 | |||
| 4ed6fbe5fe | |||
| 026260502b | |||
| c45bc91389 | |||
| 18113900fe | |||
| 69358f8372 | |||
| 8800c6d2e0 | |||
| b63de6a902 | |||
| b1cec7d6a5 | |||
| 08821e499c | |||
| 3ca84cfc49 | |||
| be8ecce995 | |||
| 775c3465bb | |||
| 7484e3c032 | |||
| b93e79274f | |||
| fee6a96350 | |||
| ac2c09108f | |||
| 5b13785115 | |||
| dd3c53edac | |||
| 904071ebe6 | |||
| 7e0aeb425d | |||
| f4e985b5cc | |||
| 03134832e9 | |||
| dbb0f66094 | |||
| 4631cd1fe3 | |||
| bc07ad5909 | |||
| 8b0c4a0efb | |||
| eac77a6695 | |||
| aae8f1c706 | |||
| 1e281f6838 | |||
| aef89e4a26 | |||
| 8a1bc98d4e | |||
| 8b2d5959ec | |||
| 4891446dc3 | |||
| cace8f232f | |||
| 88b8b24629 | |||
| b15ba8c5e3 | |||
| 58c60d85e7 | |||
| 529fe93ab1 | |||
| 600438d02e | |||
| 21a357c433 | |||
| 9c05077c26 | |||
| d49d6936a5 | |||
| e6696f78f4 | |||
| f2da3ba6ae | |||
| db5de4c6e4 | |||
| ca042b3647 | |||
| 87fd3521ea | |||
| b3c66848e2 | |||
| 606aa29381 | |||
| 2d25150a21 | |||
| ca7a457094 | |||
| 23ab3e3ec0 | |||
| b11a8459d8 | |||
| 057eb0f2a5 | |||
| 8de6c5aad1 | |||
| 6fda2a0c57 | |||
| efeeba265f | |||
| 35ca7fdc48 | |||
| 959e3f68a4 | |||
| f20b3211e6 | |||
| 9b5302ebf2 | |||
| 19f90f56be | |||
| a69d9a9f20 | |||
| c5e113d0ca | |||
| 1df90f8fc7 | |||
| 8e6040ad6f | |||
| 21f107d734 | |||
| e6d40f0c17 | |||
| 6b8bb46790 | |||
| 05c3236e50 | |||
| 4f8c20ddae | |||
| 8aa283d994 | |||
| e85b3b6a8d | |||
| 04cd9b55f7 | |||
| faf237e588 | |||
| 019abc7ad8 | |||
| db4b6fe2af | |||
| c340e6f1ba | |||
| f227a307c1 | |||
| 011a2acfd2 | |||
| c2b5b14d26 | |||
| ad79a64f1c | |||
| 91f5df1e48 | |||
| 980d6fc2ae | |||
| 5aa60b8056 | |||
| dc154a00a8 | |||
| a087acf65d | |||
| 05d0755b6a | |||
| cd86aa7d9e | |||
| fcbbcc0398 | |||
| 2bd99b5cb0 | |||
| 82d248cb8e | |||
| ce2803ebaa | |||
| e85038e888 | |||
| ceafb1c5d0 | |||
| a87858840b | |||
| f708c47385 | |||
| 9eb918f701 | |||
| b4bffd2700 | |||
| 80d7c88b40 | |||
| fb315ac75b | |||
| bee840ce16 | |||
| afa67688f8 | |||
| 0c06e47782 | |||
| ea646c673b | |||
| 5cf6684129 | |||
| cb2b9619ab | |||
| b8c2a57829 | |||
| 718a22f574 | |||
| 324cd886a2 | |||
| 4cf655785e | |||
| f686d70157 | |||
| b936b4a2ce | |||
| 026839d7e7 | |||
| 830a4a741e | |||
| a3d9ed19a4 | |||
| 8f9f5fd5b0 | |||
| 805267f97f | |||
| 4a1cd159a4 | |||
| 7129c7c7af | |||
| ecc9fda28b | |||
| 7f7c3cbea5 | |||
| fc8cd39d1a | |||
| 589c66bb12 | |||
| 16fe5a91d5 | |||
| b57b7a65ee | |||
| 019e93e76c | |||
| 69415ba2c3 | |||
| 1607ad2111 | |||
| f1a0c46a29 | |||
| 9b81b805be | |||
| 8379bb818e | |||
| c62f2a0965 | |||
| d383e71aca | |||
| dbe93f598a | |||
| 2bc5f7132d | |||
| b17f339011 | |||
| e2b04d543a | |||
| 0601f98265 | |||
| bae5650e7a | |||
| ff40b4dc4a | |||
| 6098f74d8f | |||
| 45c153321c | |||
| 19aedebff1 | |||
| 5392f616b4 | |||
| c4ccb9d493 | |||
| b0726b5008 | |||
| 9c221419ef | |||
| fddf0c47fb | |||
| b17862e212 | |||
| e133898554 | |||
| ee5a0e7edf | |||
| b538c06fcb | |||
| 49cbaa579f | |||
| 20bd9b525b | |||
| 3038fc523c | |||
| 52a85e3f82 | |||
| 3df1c4b507 | |||
| 6ada02e245 | |||
| 9184a0d902 | |||
| 4bc4e1b76f | |||
| cb192d3c74 | |||
| 11dabf9a68 | |||
| ea4dfd003f | |||
| 326b14402f | |||
| d71ca8a9e0 | |||
| 45259f0773 | |||
| 83337eee57 | |||
| aed861036b | |||
| 3ff05c0947 | |||
| 5b2a53a2b4 | |||
| 3f6aee1443 | |||
| dba70d4ce2 | |||
| ab3dfd48ae | |||
| ab59e9134b | |||
| 56921f0961 | |||
| f9005c33e9 | |||
| b7c5b36556 | |||
| 33deef9f2c | |||
| d5c3cb1b7f | |||
| 19d8f2bbac | |||
| 1aa103d8d2 | |||
| 75f630e45d | |||
| 1defde0f5e | |||
| e38595e735 | |||
| c0e16ac98c | |||
| b33429317c | |||
| e3f7c848d7 | |||
| 5f2ed4450e | |||
| 11e25ea9a2 | |||
| fb7184e6fb | |||
| 3c77a8772a | |||
| 5ce787c6ea | |||
| 67189ad323 | |||
| 3bff5430f6 | |||
| 17efcad6fe | |||
| d7e6cee19c | |||
| 116085e927 | |||
| 96e6e7b1e8 | |||
| 061199ec3c | |||
| 2d67a35af5 | |||
| 4cb6ad9e70 | |||
| d639a29501 | |||
| ac02f30dc8 | |||
| 05a20ab56f | |||
| 964aa6d94a | |||
| 1a17b138ce | |||
| 2e27e247f3 | |||
| ef8e4d6d35 | |||
| 8bee38367a | |||
| 0318c65847 | |||
| ab5b69bbdb | |||
| 9f4c5af665 | |||
| e275301e8b | |||
| c8a7820cb3 | |||
| 17dc1bda19 | |||
| 8d9de37099 | |||
| 29b403da45 | |||
| 1fd1ba2ada | |||
| 5fbce3b928 | |||
| 9019bce843 | |||
| fd2d106f2a | |||
| 64a1c83acc | |||
| 8cc250e19d | |||
| dbd737c87d | |||
| 33398e78cd | |||
| 79c6d0180c | |||
| e7a198dc0c | |||
| ef08c35c6f | |||
| 41b816538e | |||
| 87d9fe24f5 | |||
| d11c95a23f | |||
| 3c9b846116 | |||
| c701e2e6d3 | |||
| 11b490c77f | |||
| 0544ab2617 | |||
| 5c6d3724b0 | |||
| c34d4e777f | |||
| 588d783a55 | |||
| 79f74fdff4 | |||
| 6f5dbb782e | |||
| f0ae9b0100 | |||
| fd260a023c | |||
| e775bcac3c | |||
| 641c852ee2 | |||
| ab679c2216 | |||
| 96420a75ab | |||
| c4263692f0 | |||
| 0074b71cbf | |||
| d219c2f462 | |||
| 4700dc0375 | |||
| a248bbc46b | |||
| d60affc1b8 | |||
| 8c0f5b4e31 | |||
| a617f6cc55 | |||
| 9e66026bdc | |||
| dc90115a1b | |||
| 13b250d291 | |||
| 834ab22923 | |||
| 17be68a39d | |||
| 48ee03e278 | |||
| 5b86d8b837 | |||
| 521fce59ea | |||
| bbe1ba7328 | |||
| ce7f20219f | |||
| aad33bd1b7 | |||
| 9923c7e577 | |||
| ae870b1cc1 | |||
| e3cb199540 | |||
| d9d19fdc63 | |||
| f506882bf8 | |||
| e782183a49 | |||
| 6699c4d8af | |||
| f027cbf170 | |||
| e0012d9b81 | |||
| b2ad957d29 | |||
| cab334ed73 | |||
| a99c1e96d6 | |||
| 261ac8b2d6 | |||
| 399237e781 | |||
| 4b29f02f1c | |||
| 558da5528b | |||
| e3a00c2cb4 | |||
| 12deaa80a6 | |||
| a6507768bc | |||
| 8f19ab066c | |||
| 17decea576 | |||
| a2442add5b | |||
| 7dbe95fa92 | |||
| 3339a2874d | |||
| 393047dec5 | |||
| 55fb3d4e8e | |||
| 047180edc9 | |||
| 1b0a388eb3 | |||
| d50e559f97 | |||
| 835aafcb17 | |||
| 48ad9ba3d7 | |||
| 3675e95970 | |||
| 1ca13f4ce9 | |||
| 40aa6ba96a | |||
| b4dc1e1555 | |||
| 3effeb7dc4 | |||
| 8c11839e4d | |||
| 41817995bd | |||
| 8c9799d64c | |||
| 3a5e4ffa91 | |||
| 02afcc7d4b | |||
| e9007429dd | |||
| 664d920dd1 | |||
| 5a8299f1a5 | |||
| 6017fead19 | |||
| 69050ed338 | |||
| 2664848545 | |||
| b18f0ff738 | |||
| 1171c33f7a | |||
| 6deb2bc7a9 | |||
| 7bda13aba6 | |||
| 75719c3e0f | |||
| 2fb033b5ba | |||
| 9f1db7a707 | |||
| a93de99d42 | |||
| 3d5774ac9e | |||
| 1d8e0398e9 | |||
| cf76375ce0 | |||
| 284e2bb911 | |||
| ecb790c179 | |||
| 467b75f2dc | |||
| 1f728cab92 | |||
| 66b17aa019 | |||
| 27a6d1f878 | |||
| fc67dc6497 | |||
| fedd9a981a | |||
| 47e972a66c | |||
| 36bd4b5408 | |||
| f502233ddc | |||
| 59c1edb623 | |||
| 9dd00c7731 | |||
| 311edb4e4c | |||
| 924b8629d8 | |||
| 9e11da1fa5 | |||
| b760fa0ff5 | |||
| cbce2f46c3 | |||
| 7aa5a79c86 | |||
| fc029a9b9e | |||
| 86ef80ae9a | |||
| e5b475ad89 | |||
| 2c6858f149 | |||
| c1bff0b2ea | |||
| 39892c98f9 | |||
| b15487ec03 | |||
| 303119e113 | |||
| 09b729beb5 | |||
| 6b73a77b2d | |||
| a2449ff6a7 | |||
| b1b7522b80 | |||
| 608b0e7b93 | |||
| 7c61b9cf7e | |||
| 50a973409a | |||
| bfea882416 | |||
| f5e8fe836e | |||
| c4664a185f | |||
| c5f093c42f | |||
| 64f369b5de | |||
| 4b5653c09b | |||
| 837e739ec6 | |||
| b780ee8373 | |||
| fe5bfbf76f | |||
| d924617672 | |||
| c545c7ca70 | |||
| 9eb9f3a117 | |||
| 7862fd9679 | |||
| 17402e8475 | |||
| aac77440db | |||
| 13c9c4bea5 | |||
| 68c1171294 | |||
| f4f01913fe | |||
| eb5908d5d2 | |||
| e0c36498e6 | |||
| 913710dd99 | |||
| 2f0d96d030 | |||
| 265802acb1 | |||
| 045f31a0dc | |||
| 9f6ed4fb33 | |||
| 434117c771 | |||
| cd95b8724a | |||
| ec2a4d473e | |||
| 47eaf3cd58 | |||
| d7b23a8634 | |||
| 6db7972f04 | |||
| 6840ee077c | |||
| 3c85bcc3c9 | |||
| 759eca6479 | |||
| 4488f174aa | |||
| 991a255041 | |||
| cfef635e1b | |||
| d3027e1fe8 | |||
| d99ea1c6b4 | |||
| 9af214007e | |||
| 0541b7f3c5 | |||
| 63fa774af7 | |||
| 4b19b36de1 | |||
| 5715df6b18 | |||
| 22bcacc715 | |||
| f1e270ca9d | |||
| f535e7535c | |||
| 912c5e13e1 | |||
| 4eb44ee2ea | |||
| e41a2beb65 | |||
| 1f6ba31a3f | |||
| bcccc909c5 | |||
| b3a11030f2 | |||
| baaf76668f | |||
| ca37ae6794 | |||
| cf21d64aa8 | |||
| 36e2533164 | |||
| c04d79d9a0 | |||
| 9084b4e7aa | |||
| f724da7e84 | |||
| 4b8f47e2b4 | |||
| 35ecbed29d | |||
| 30ed153ad1 | |||
| a1098989ff | |||
| 42bb63e07d | |||
| 683aae5ed5 | |||
| 49ef274a83 | |||
| af16bba1ec | |||
| 781086608e | |||
| c1625e5c27 | |||
| 0e05f9fd73 | |||
| ef7595bb06 | |||
| e9c98b03b0 | |||
| 75370f7855 | |||
| 4a79e13410 | |||
| 220061f022 | |||
| 090c2c0891 | |||
| 6c317b7f28 | |||
| 3788fbf607 | |||
| 382854c04c | |||
| c2fae3bad8 | |||
| 92ebd39391 | |||
| f53a32a6b4 | |||
| 6c882e6605 | |||
| ca85dfc6ff | |||
| e22ecc6b6d | |||
| 2608dd2d64 | |||
| 7d20d24249 | |||
| a3ac3692af | |||
| 0070c8f843 | |||
| b360fc8308 | |||
| bf9ba65ac4 | |||
| 43abfbc537 | |||
| 2700f0acf6 | |||
| 9156bed961 | |||
| 71dc0bac56 | |||
| 40f55b2964 | |||
| 9307f9f345 | |||
| dfb918adc3 | |||
| 2f87a4859e | |||
| 191f73e0d0 | |||
| 263e55f25d | |||
| 5c55dce13e | |||
| a1a6ec6dfa | |||
| e1edd84700 | |||
| 07ee256756 | |||
| 48888e530e | |||
| 4ef50bef55 | |||
| 486369e97c | |||
| 67994f7a53 | |||
| f027ddaf35 | |||
| f3b27d1e06 | |||
| 92e18b32dc | |||
| 4030ec9c8b | |||
| 497c2dc8df | |||
| 8a1d34c419 | |||
| caab5befaa | |||
| 0d316e3d3e | |||
| d8fd12103a | |||
| 8f9a682d34 | |||
| 102d5acf09 | |||
| e2ec8952e3 | |||
| b4eff9b996 | |||
| d050261fa9 | |||
| d29330815e | |||
| 801b4022de | |||
| bcb4071993 | |||
| e78fbd1dff | |||
| c543358826 | |||
| ff2954839b | |||
| 1a91b88968 | |||
| 5724462c2c | |||
| 0a0489750c | |||
| f46190509a | |||
| 25ec81b6c7 | |||
| a50802a63f | |||
| ce1d374a82 | |||
| 6dca8ac460 | |||
| 4dc21674d5 | |||
| 8805dd8c01 | |||
| 73b624e761 | |||
| c44fd972b6 | |||
| ff344bc110 | |||
| b0e2a38325 | |||
| fbb741ab10 | |||
| 75321220fd | |||
| 43198b0425 | |||
| 4aa2c03ff0 | |||
| 3a3be36f4c | |||
| cb91c4292c | |||
| 80722ce145 | |||
| 07bfa5532e | |||
| cea1a3ff91 | |||
| 270be2df7a | |||
| e73b969066 | |||
| 98e2154f0f | |||
| 2c0549a772 | |||
| acb9bc8cc5 | |||
| 6f44aa39e8 | |||
| c706618229 | |||
| c26b571b1c | |||
| cb3075b084 | |||
| 66fb21bd0b | |||
| 498109bd53 | |||
| 6711ab5f9a | |||
| ac8fa58845 | |||
| 095f656998 | |||
| 9e30c4f7dd | |||
| 56a2eeac77 | |||
| d104f2f4a7 | |||
| e8367ad241 | |||
| f17cd142d5 | |||
| db4c6af472 | |||
| 87689ca733 | |||
| a1a5d85979 | |||
| b1459a43ef | |||
| e8edc554a6 | |||
| bd8aca83ac | |||
| 7ebc1cfac5 | |||
| 2422204d6a | |||
| 6c98c3c662 | |||
| 1d1310f034 | |||
| 841888c480 | |||
| 8797381f44 | |||
| 92e89ffbcf | |||
| 52c031f160 | |||
| 155113b75d | |||
| 2a863025c6 | |||
| b6763ce89f | |||
| f346cd6b8d | |||
| 0c47412c75 | |||
| ea1ef3dbec | |||
| 61fd62ff81 | |||
| 32197ea903 | |||
| 65de184d88 | |||
| 52764045ce | |||
| c2da4376e0 | |||
| 4b0db6c472 | |||
| 78ebf8f117 | |||
| 3ec89a89df | |||
| 9e6b72bf38 | |||
| 747723c8fb | |||
| 40cd4629db | |||
| 52a893a811 | |||
| 7cc94e1e1e | |||
| 88945a6d6d | |||
| 7203c5aaf0 | |||
| c305058c46 | |||
| 91bbf7d1a8 | |||
| 2d5857f145 | |||
| 5de189bfa3 | |||
| 91af9a411d | |||
| 9a9ed124f5 | |||
| 44fc820f99 | |||
| 4e5442d972 | |||
| 5a8e5a9785 | |||
| 960c5da3b2 | |||
| b5fa54f91e | |||
| 2246aede4b | |||
| 4b2d409c69 | |||
| 684511ff06 | |||
| 88b328df7e | |||
| e3583dd04e | |||
| e484a2ebb5 | |||
| 5caf05cfa1 | |||
| 72f86258d0 | |||
| 874cb3b779 | |||
| f21e0228b4 | |||
| 00a28e743d | |||
| 2596c25ccc | |||
| 01cd82bc6a | |||
| 582aafa552 | |||
| d2a6a8b283 | |||
| f242e460ed | |||
| 61e7d4f807 | |||
| 95e08253a6 | |||
| 202a4fa6f1 | |||
| 576f46cb88 | |||
| 2d73805ca3 | |||
| eedfa550a6 | |||
| 4ca718adc4 | |||
| 7c4ced8f46 | |||
| 7018f4ab25 | |||
| fda13875ef | |||
| 8005917452 | |||
| f2c215311f | |||
| a13cf0e1e0 | |||
| ff60bbac9d | |||
| 16f569136b | |||
| 27c172361f | |||
| 1e0d6b9d4a | |||
| b67cd94ee2 | |||
| c6764490c6 | |||
| 18580624e6 | |||
| 7b333a34b5 | |||
| d0707e183d | |||
| fa3b246de5 | |||
| df28d87d25 | |||
| fc68bb3ae0 | |||
| 82c530da95 | |||
| d250e7387c | |||
| cbc74815d8 | |||
| e9b802deb3 | |||
| 377ca0c678 | |||
| 972aef7a9d | |||
| a35559be65 | |||
| 0e6b43a769 | |||
| 8c8a68d3ae | |||
| 4d74b5cdad | |||
| b1ace49f9a | |||
| 91eee8587e | |||
| e9132abc25 | |||
| 640d13af99 | |||
| 50e0f6353a | |||
| 444eac5c6e | |||
| 30f2263443 | |||
| 25eb6de220 | |||
| cebdc44689 | |||
| 23f5c2e03f | |||
| 4d4a6ede21 | |||
| 6a920fe623 | |||
| 631faa2046 | |||
| a4e853e1d4 | |||
| a449c5f8c2 | |||
| 19d6dbaa52 | |||
| 8820619e82 | |||
| fb33bc7e07 | |||
| 57d1fa8410 | |||
| 838e38d84c | |||
| fc29056530 | |||
| 4e967c979c | |||
| 62a34848c7 | |||
| 01fe6cc542 | |||
| 3fdc25777d | |||
| 3b7d6f8334 | |||
| 4b3c8b2969 | |||
| ad80d69369 | |||
| e2d2249686 | |||
| dc64c34ccb | |||
| 0bf50659ab | |||
| ec26b16ddf | |||
| a044b74a1d | |||
| 1e7a1dce90 | |||
| 41c2772cff | |||
| 1bba2bc0ed | |||
| e11c523a75 | |||
| b911a890cf | |||
| c8f69c0b79 | |||
| 8a6248f120 | |||
| 340fa6c63e | |||
| 1177bf39a2 | |||
| 88b310c394 | |||
| 973de2db55 | |||
| 1fe92f10c1 | |||
| 0e2e906b24 | |||
| 4667f8be03 | |||
| 4a51ac7a74 | |||
| 6099efe41a | |||
| 4254d595fc | |||
| e4fbbd56a9 | |||
| 069ca4a89d | |||
| 4290e8e56b | |||
| e3ba08fbbc | |||
| dd84e51161 | |||
| bca8568d64 | |||
| 5407717534 | |||
| 74ef760591 | |||
| b435b582bd | |||
| d46021a05e | |||
| 1b31d0622e | |||
| a416fd562b | |||
| 323a096dd7 | |||
| 628dd7bf41 | |||
| 71c6d71cae | |||
| e049edd449 | |||
| 68206a6e19 | |||
| 56797948af | |||
| c0af2f25a1 | |||
| 0fdfc3ff53 | |||
| dc12b1df00 | |||
| b13f5aebfd | |||
| 338301bb5d | |||
| 72a0931663 | |||
| 67584b9cc3 | |||
| 1bfaa28f9c | |||
| cbe9b59222 | |||
| 276b52f0fe | |||
| 07f49bcc37 | |||
| 09fac77ce0 | |||
| 102704e91a | |||
| c5fb351baa | |||
| 98f8d4414d | |||
| 1dddcd4925 | |||
| 2666a271a5 | |||
| e277de6e3d | |||
| daa17b3287 | |||
| c7f887131e | |||
| 58546b80d0 | |||
| 466f749b71 | |||
| 4c2a83c470 | |||
| d534ab18c7 | |||
| 64aaed833b | |||
| 2f05be599c | |||
| 7371f8dd3a | |||
| 79aefe9707 | |||
| 6bc80577ee | |||
| 837764190f | |||
| 61948d70e3 | |||
| 9fb0385694 | |||
| 193de8d3a9 | |||
| 1f2b3512c1 | |||
| ddc5bd3b36 | |||
| ee828d454a | |||
| 2916a73f4c | |||
| ae69af7e70 | |||
| 1c6459fe65 | |||
| adaeb42416 | |||
| f1e1daa194 | |||
| 401e89ef78 | |||
| 59e0bd467c | |||
| 7f4397f8ca | |||
| 32830b93f1 | |||
| cdc0d5623b | |||
| e78b415832 | |||
| ff1379fd29 | |||
| 3820c15ecf | |||
| 26ef33e4f3 | |||
| 0534a4ed1b | |||
| f29a24a915 | |||
| cecbcd941e | |||
| 6be99d6397 | |||
| 4e5947af51 | |||
| 4204b2170a | |||
| 0a5ad489b6 | |||
| 5de34a5c99 | |||
| 08da6b8800 | |||
| 02b283be78 | |||
| 10c49c0fd1 | |||
| 9ecc0f5d95 | |||
| 972b59b99e | |||
| 34bb05bd88 | |||
| 37fb21f726 | |||
| b42efa4a07 | |||
| 20b20738a7 | |||
| b28a191c4e | |||
| f92b620434 | |||
| ae6e2cca27 | |||
| bd920eef1f | |||
| bf25cb68da | |||
| 0b063f6b8b | |||
| ed6d4e5f6c | |||
| accfa325b5 | |||
| b6ef8d95cd | |||
| c1144e3810 | |||
| 8663fd402b | |||
| d8134aa168 | |||
| 702b3e8473 | |||
| f34052fd31 | |||
| 27d75a269f | |||
| d208a7fc5f | |||
| 702e16e3df | |||
| 6381018658 | |||
| 12050b14f0 | |||
| 1c191b2278 | |||
| 2d4a4f1736 | |||
| cd38fb9b4c | |||
| 7941b16ec4 | |||
| 3ff517e76e | |||
| 9559b26310 | |||
| 56ea4b8741 | |||
| a489691151 | |||
| 6dabfcda6f | |||
| 0b7b35f800 | |||
| 0bfcb5071d | |||
| ceb162eb01 | |||
| 0ffdf7c0f1 | |||
| 13b6db8eb4 | |||
| 481acb2a1a | |||
| 683092140d | |||
| 1bb8c2d1a5 | |||
| 60fd3b0786 | |||
| b307a177f4 | |||
| 059430bd0a | |||
| 530b60cbc2 | |||
| cd4abc4e9b | |||
| e6a21cc487 | |||
| ba8577f268 | |||
| 6c5fc153bf | |||
| 07f15b41a2 | |||
| 8375638d76 | |||
| bed7543b46 | |||
| dc55236263 | |||
| 5f3427c5d1 | |||
| 66e5af185d | |||
| 0ff611e033 | |||
| 737cadaabc | |||
| 9fb2fbaeec | |||
| 51e817a3a2 | |||
| 59c93b59bf | |||
| c18ef051fc | |||
| 1ac5c9acbd | |||
| a034ca171e | |||
| 977682d37f | |||
| 0bafe263d7 | |||
| 1a8fced80e | |||
| 1c4d0b5e99 | |||
| 844a2b457c | |||
| ccf06f2216 | |||
| f630a9f297 | |||
| 2f71c93b53 | |||
| 92032a17a8 | |||
| e531456d42 | |||
| f0b2d2fe4d | |||
| 427500220d | |||
| 32e19ead74 | |||
| d11adb6f43 | |||
| f6155a50f6 | |||
| 4efee9445d | |||
| 20746a433f | |||
| 31dacc4206 | |||
| 88e5c59a85 | |||
| cf74920b36 | |||
| 972c900b58 | |||
| 12d5fd79f7 | |||
| a29f6979b2 | |||
| 3a7146c77b | |||
| b178d8f629 | |||
| 0c94ee62a3 | |||
| e7562898cd | |||
| fb73ab6878 | |||
| 38f978791d | |||
| 5dd60de57d | |||
| 5efbfc2dba | |||
| fcd1dbad89 | |||
| ad521bf4c2 | |||
| 8152fa44e0 | |||
| e217bf9e37 | |||
| bfad21f811 | |||
| 81e68abce3 | |||
| 7963bb352d | |||
| 14d3882059 | |||
| dede508e89 | |||
| ea39b69f65 | |||
| eafecd36bc | |||
| 198c9a2507 | |||
| d07563013b | |||
| 9e967832cd | |||
| bfe1987cd9 | |||
| 1045538f1f | |||
| fccf08edcf | |||
| f43fe366b5 | |||
| 0f75f2ef9c | |||
| 2cdc68f9c3 | |||
| 6a7d58e22e | |||
| 203829c1cd | |||
| b55e6c4ef0 | |||
| 8d779e8aec | |||
| dd1d48f688 | |||
| 8d14dc9ee3 | |||
| 5849ea8e63 | |||
| 20afebf339 | |||
| 20eaba191e | |||
| ba58d3c544 | |||
| a8b9d8e3ae | |||
| 83d1e61b2f | |||
| 8e0fc8d460 | |||
| f547fa732f | |||
| e24b1519a4 | |||
| 3028fe9c87 | |||
| 0b970b05b6 | |||
| f7bfb1e49e | |||
| 371ca009e9 | |||
| 4a0f848551 | |||
| 5e8b7b2a62 | |||
| 0f27b703bd | |||
| 61e19c30cb | |||
| c82bc35202 | |||
| 65934227c3 | |||
| 7519becd43 | |||
| fe83c15bc6 | |||
| 07e6b47fa7 | |||
| b026e1c2f7 | |||
| f8194d9418 | |||
| 1ecd7f274f | |||
| 66bf0ec7af | |||
| 1b22df2b7b | |||
| 9f993f1f67 | |||
| 975518bd88 | |||
| 66a863456c | |||
| 91290c0d25 | |||
| 8a23e89c87 | |||
| 9e9cf85ba1 | |||
| 3dd365bbea | |||
| 33a824b980 | |||
| 8571884304 | |||
| 4f1067e66c | |||
| 7b5b851db0 | |||
| ed0be0cf84 | |||
| d3775e5cb1 | |||
| 2c8f658810 | |||
| 5c52f5f579 | |||
| 0a81bb3fdc | |||
| f33196bc51 | |||
| 6beb90a835 | |||
| 9d45e6acd6 | |||
| 516c464458 | |||
| 6ad3fb16b3 | |||
| 277fdd9b8c | |||
| 7d56993b39 | |||
| 4e1442fcf6 | |||
| 4777bf3e75 | |||
| 14cd37ec56 | |||
| 8bcdfd50c9 | |||
| 6776df8e80 | |||
| fbec079c9b | |||
| 93f6bc3780 | |||
| dde8f23cc3 | |||
| 7cfbd0da95 | |||
| a27ddfaaaf | |||
| b53f616015 | |||
| 22dc175879 | |||
| 5f23e4699c | |||
| a1bd258a7b | |||
| 39a9c54589 | |||
| 5f68370e07 | |||
| dae2de703d | |||
| fa19c40868 | |||
| 1df69d259a | |||
| 90dda0ca68 | |||
| e2d138cac6 | |||
| 15f968d5f8 | |||
| 4a3b68de8f | |||
| f6aec7f763 | |||
| 212b6c3a0f | |||
| 820256d451 | |||
| 3aba538db3 | |||
| 4820cf8cac | |||
| c289effba0 | |||
| 3edccf496a | |||
| 97b4171b3e | |||
| 4a073a7ba5 | |||
| 9f275d57a9 | |||
| d23bbaeb06 | |||
| c64f7a9ec4 | |||
| 214a9df382 | |||
| 90f6620f1e | |||
| 45f3a2f909 | |||
| 5904378170 | |||
| f6e8048d9e | |||
| 5afca17d27 | |||
| 2d7f5ae279 | |||
| 458384d658 | |||
| 159b98132d | |||
| 349bb2730a | |||
| c13813348d | |||
| 26e70d6b30 | |||
| f6d3b50b08 | |||
| 50ee489079 | |||
| b60e5f40ab | |||
| 5b1fdb7b37 | |||
| 65d4015300 | |||
| b692cd109e | |||
| 0f90f055ba | |||
| 28d5ce288c | |||
| c701bf279f | |||
| 6039066e7f | |||
| c9a2f8b170 | |||
| b34a36d853 | |||
| f8f76f6806 | |||
| e25ae546fc | |||
| 5b73bf3e5d | |||
| c16b093bd7 | |||
| e406f32386 | |||
| c4e7c149a4 | |||
| f91edfabbb | |||
| f410004d45 | |||
| 49e238d580 | |||
| 489d188966 | |||
| 79fb7bab0b | |||
| c410954bad | |||
| 53a8a7d50f | |||
| 1c7f95c0ee | |||
| 1717fcf499 | |||
| b25453cf87 | |||
| 07b596bf30 | |||
| 1166947c21 | |||
| 6e7b9ca6c0 | |||
| 5b1e3537cc | |||
| 44843d418d | |||
| a103ffa038 | |||
| 712335789e | |||
| cdf8186f44 | |||
| d06942d602 | |||
| 45ac3a60dc | |||
| 4f244da3ec | |||
| 4eefa05d3f | |||
| 40fb31099a | |||
| bcd85f5397 | |||
| 89aeda45c6 | |||
| 7581e5ffdc | |||
| 7046fa3224 | |||
| ef392785e8 | |||
| 5bd029115c | |||
| b6f42b25dd | |||
| 75dd9625a0 | |||
| fd6110679e | |||
| c761019aca | |||
| 853363fdf5 | |||
| f7753f8be3 | |||
| d2dfa29556 | |||
| c0a88b7f4e | |||
| 150e5fede4 | |||
| bb3ec322fb | |||
| f3ee164a7d | |||
| 035cb9fe08 | |||
| 58d0018174 | |||
| 52ed0f8615 | |||
| 2a46513dfd | |||
| 907567182d | |||
| 46cebcd1ca | |||
| 40198f95dc | |||
| 736b934b18 | |||
| b009811ac9 | |||
| ff6612f9d0 | |||
| 565d446b1d | |||
| 044d334398 | |||
| e31ef2dfb5 | |||
| c20566083a | |||
| 9845553a5f | |||
| 03737546fe | |||
| 579cb00c98 | |||
| a307950213 | |||
| 0f5c469be6 | |||
| 6f7e409e9a | |||
| d707912d81 | |||
| 97ab680f4e | |||
| ea35a29bd8 | |||
| 63c182bc3e | |||
| b1a0a12a5f | |||
| cc242230be | |||
| aef9211ea8 | |||
| de5d557882 | |||
| fa9adf199d | |||
| 0807066f1f | |||
| 142e79941d | |||
| 5e9ce38a24 | |||
| f42e6373c4 | |||
| bc46609caa | |||
| cc44abe2d3 | |||
| db6398acdd | |||
| 6661bde608 | |||
| 69f6bba964 | |||
| 2a4e722f0f | |||
| dd20828ded | |||
| 5993dd588c | |||
| 30f86e2437 | |||
| 3ef91e16d8 | |||
| 45e9b3ac68 | |||
| a076e3f0fc | |||
| 99bff04ccc | |||
| bd906e619d | |||
| 34882cc438 | |||
| 5ac00e3465 | |||
| 622dd065ff | |||
| c5c98a6ac1 | |||
| da423ed508 | |||
| 11c4337cfc | |||
| 458164384d | |||
| 13c7f55a79 | |||
| 4ab675863a | |||
| 5414b3b39d | |||
| c54db30dc8 | |||
| f11103bfcc | |||
| b56936003d | |||
| f61604a51e | |||
| ae77f900ef | |||
| 645842f0fd | |||
| 39d3640973 | |||
| 66aa9c4831 | |||
| 7de0ca2048 | |||
| 5bbc5cad9f | |||
| 0a7a80d5a8 | |||
| 33d1a33a17 | |||
| 7f130949c8 | |||
| ba7ee37899 | |||
| f8863d5c24 | |||
| 1d7954c831 | |||
| 2bb4e91dfd | |||
| abe5bf4240 | |||
| c493bf7866 | |||
| 06bf0f22be | |||
| 02d9fe1d30 | |||
| c3091c5aa4 | |||
| 677a427f1f | |||
| c416dd01a7 | |||
| 662fcb426b | |||
| fd0fe9e225 | |||
| 54a03b234a | |||
| 0ca8613896 | |||
| 4b1817719e | |||
| 295c591e95 | |||
| 9df0480b78 | |||
| 5260f40451 | |||
| 0cbe35e41f | |||
| 502745271d | |||
| 95baa3cd27 | |||
| 8fe4a29176 | |||
| 1a1a0e7324 | |||
| d00d07a1c1 | |||
| f27db16e30 | |||
| d4e107b3cd | |||
| 881b60c85f | |||
| c5e1aade12 | |||
| 28198a6f40 | |||
| 30a01e26de | |||
| 8712703f7c | |||
| e3a8631faa | |||
| 3eab51ce79 | |||
| 9db0fe0795 | |||
| 228af037e3 | |||
| 654f250fd8 | |||
| a8693d9d68 | |||
| f9f345e428 | |||
| e678706414 | |||
| 8018259480 | |||
| 9f713781cd | |||
| 48d56dc5c0 | |||
| 701dfa09b4 | |||
| d965648fd7 | |||
| 08c15e7203 | |||
| 9b9e52d0a2 | |||
| 38cc8fe7dc | |||
| 590fac0fa9 | |||
| 9590c8aaf0 | |||
| e2b79e4e7e | |||
| 2df588f95a | |||
| 7c3af91b42 | |||
| ad2f537887 | |||
| df36f0dab4 | |||
| 608d7faa29 | |||
| 7845957d0d | |||
| 13c5c4e4f5 | |||
| 62114028d5 | |||
| 4cc4b28c47 | |||
| fad9b4c67f | |||
| ade3b3a021 | |||
| d8c4101fdd | |||
| a12c250f2b | |||
| 57eef2d832 | |||
| b67a179a54 | |||
| 06044b39c3 | |||
| d16cf26c5f | |||
| b060c5af38 | |||
| b28bad651e | |||
| 5c4b7a3213 | |||
| 7f21c591ae | |||
| 65b24f595c | |||
| 9d9c2720c2 | |||
| f845100062 | |||
| e6155f9e37 | |||
| e9590e9093 | |||
| 49f2d1501c | |||
| c6819e0450 | |||
| f518ea95f4 | |||
| 92c6332143 | |||
| 0df1a7da21 | |||
| 487a9c0967 | |||
| fb89761671 | |||
| d1d3ae074d | |||
| 7dedaf90c3 | |||
| 687b98a09d | |||
| c6b2e9873c | |||
| 1e80491675 | |||
| a727da9193 | |||
| 0b7754581a | |||
| 452e8ea385 | |||
| a189de9a2e | |||
| 8632ca6e37 | |||
| 293860b6c5 | |||
| 07667172cd | |||
| f5240cdac6 | |||
| 7529d2b638 | |||
| 03fc12e888 | |||
| e05a50528e | |||
| 356ee90417 | |||
| 5dced57724 | |||
| 8ec3b88c5d | |||
| d7ef128510 | |||
| bb2502409b | |||
| aa6aab4245 | |||
| 12aab0caeb | |||
| 2f316c558d | |||
| f447273b75 | |||
| c8a18d51e6 | |||
| d77af1e67a | |||
| 81c95224d1 | |||
| a0e66291df | |||
| 5733f46f4c | |||
| da2128feff | |||
| d413faefdb | |||
| 0d1d767d96 | |||
| 5619554023 | |||
| 53880a4bb2 | |||
| 812ae227b6 | |||
| c6b44098ac | |||
| 92b95a98d0 | |||
| a3505ff42d | |||
| 15b2e3ff1d | |||
| 1845d1ac55 | |||
| 463819caa7 | |||
| a0317d9587 | |||
| fa6ce0cb70 | |||
| 7471ff4b0d | |||
| 65a5bfac88 | |||
| d2321410f8 | |||
| f90b5a99cd | |||
| cc4656b36a | |||
| 34bc63a146 | |||
| 09bd91a588 | |||
| bb3e082e5b | |||
| 60c863f829 | |||
| 2d2a73bf52 | |||
| 7b9f73709d | |||
| 1dc89f642d | |||
| a78b59010a | |||
| a2e1d94fcf | |||
| b181c83b93 | |||
| cf7c84c4ba | |||
| 6e5230f9f9 | |||
| f03f7c0acb | |||
| aa9b807b82 | |||
| fa9921e091 | |||
| a9a6b2de48 | |||
| 1ef746658f | |||
| fe0099b497 | |||
| 7e9c4146a5 | |||
| 3e978c64e4 | |||
| 13c5920a46 | |||
| 3b19203fd2 | |||
| 84cd05b218 | |||
| 9b9a9642ee | |||
| 639d2317ed | |||
| 52379d7655 | |||
| 29827362d6 | |||
| f33315f610 | |||
| aec92a41da | |||
| d570811ddc | |||
| 9cd015a218 | |||
| 8d2cc5096e | |||
| 7ec0bf69f3 | |||
| f3a8306107 | |||
| 2b29e9934c | |||
| 4b1104e463 | |||
| 60b9ef959d | |||
| 0074c2cf57 | |||
| b0204ab54e | |||
| bc5b07aa75 | |||
| cfe90dbed5 | |||
| fa913655bc | |||
| af0b6fc6ac | |||
| 6347031f61 | |||
| 3eda039898 | |||
| 5447d99481 | |||
| 9d08744fe2 | |||
| 848fa257ea | |||
| 54553fb671 | |||
| aad7484c8d | |||
| ad658ead37 | |||
| c787f0e9d0 | |||
| 6f5da701aa | |||
| a01368bd60 | |||
| 16dccd75c1 | |||
| 5151b52688 | |||
| f682097e45 | |||
| 89ebffd69f | |||
| cdeb4ead9e | |||
| 79cc835874 | |||
| 8df866865d | |||
| fa7eff50e5 | |||
| 6558881952 | |||
| 9cbe2c985d | |||
| 7769c53cd6 | |||
| 7a5a47ecc7 | |||
| 492c1d77d8 | |||
| 8c315ebd15 | |||
| 6044b2cbad | |||
| 7acfd0b423 | |||
| e201aab440 | |||
| 3952768667 | |||
| fb25fa3a27 | |||
| 857ad9b180 | |||
| 14843a1bca | |||
| 6331b34cf7 | |||
| 2ebe1dfa16 | |||
| 6bdbee533c | |||
| b9886d4f34 | |||
| 666cbbce08 | |||
| 69c575a4be | |||
| c920de0d28 | |||
| 53cdf53f63 | |||
| d9dda6aebc | |||
| 3d4a9a24ce | |||
| de339d3098 | |||
| 0f83234be9 | |||
| bcd9b45589 | |||
| 19f3996e09 | |||
| 25c2cc1768 | |||
| eec7c4c61b | |||
| 25b4b049b7 | |||
| 2191eb3f41 | |||
| bebdbf7e05 | |||
| 646c091966 | |||
| 952729cb1b | |||
| c6992e2056 | |||
| 77ed79e9a9 | |||
| c4f4add0ec | |||
| 7eeb60c838 | |||
| 438861ae5e | |||
| d42cdbbc5b | |||
| 66237e1ea6 | |||
| a51c0450c3 | |||
| 7a2416bb6d | |||
| a1528e9e33 | |||
| 71cc4d535e | |||
| c06723df3d | |||
| 06b285c013 | |||
| 49c06ef0ca | |||
| 5f92357fec | |||
| 3e0dd3d918 | |||
| f19d76b08d | |||
| 22713d8f89 | |||
| 8b6b16067b | |||
| a2da0de17d | |||
| 93ff3edb6b | |||
| 7c67fd69dd | |||
| 5ef5412a55 | |||
| e88a384aa7 | |||
| 9067feeafb | |||
| ed978f69fb | |||
| 743f2465ea | |||
| 41fffa233a | |||
| e45377166b | |||
| 24939bf0b0 | |||
| 3221be4855 | |||
| 3135f1ed24 | |||
| 1b0834ffb0 | |||
| ad85740ae2 | |||
| d79d613cb7 | |||
| d8cc1f7b7a | |||
| d7c8856fdd | |||
| 9d80a332aa | |||
| e14f7b63c7 | |||
| 3bd2880923 | |||
| 6ec7e3a0b7 | |||
| 1a1fe759c3 | |||
| 2401ad7159 | |||
| a919c798f8 | |||
| 80fe66c481 | |||
| 4577dc8f44 | |||
| cf0a5305e0 | |||
| e69e6e1981 | |||
| 0febd99fbe | |||
| 6e8e3e4150 | |||
| 5d95398621 | |||
| 64cdd73b93 | |||
| 48a9236ea8 | |||
| 8b3126e9d8 | |||
| 74d497cd2d | |||
| 5070a5c598 | |||
| 2e30b08e74 | |||
| 8bf63f5f0b | |||
| 11665d18ee | |||
| a8a9fc0c9d | |||
| 098cd1b8d4 | |||
| 3166a4880d | |||
| 9d1c7136cc | |||
| e100943edf | |||
| a6fe4cdf1c | |||
| 8b5213c09a | |||
| 23a133c825 | |||
| 69c4496dfe | |||
| 8a4440c314 | |||
| 4d74cca206 | |||
| 955e081699 | |||
| b7e73422ab | |||
| a9a4ba33aa | |||
| 7116ad9f58 | |||
| 3d18bdb2aa | |||
| 4c14581606 | |||
| a9c9ec3977 | |||
| 694d1f9631 | |||
| 2b9cfae18a | |||
| 800d8380ce | |||
| 621ca28f68 | |||
| 4792241ff6 | |||
| 0b984df5f9 | |||
| 2e260155ea | |||
| b15c8a2d1c | |||
| 94ab317f23 | |||
| 02b37e1219 | |||
| 9d25848a21 | |||
| 3958768e1f | |||
| cec00cd303 | |||
| 45cfef4294 | |||
| 2a01e99635 | |||
| a9c3aee447 | |||
| c669382e12 | |||
| 19251c427a | |||
| a7aec9e2a4 | |||
| 99d7622b42 | |||
| 8728619d2c | |||
| f6d51fdfb8 | |||
| 0ffaa8d617 | |||
| 8a974172ab | |||
| 72675f7266 | |||
| 0f559050d8 | |||
| 58084774bf | |||
| c7aee7cebd | |||
| c68d135ae8 | |||
| 9ed6c99ec8 | |||
| 7d398d41d0 | |||
| 0af763252c | |||
| 1c9dbbbb19 | |||
| 2a688bdac8 | |||
| 3c53c818cb | |||
| 8e53cb324e | |||
| efbd454775 | |||
| 68c7273f56 | |||
| 8b56ff4eff | |||
| 8134eedd93 | |||
| a87ae770cc | |||
| 355da0f9a9 | |||
| 10bdd63762 | |||
| 69dc518c2c | |||
| fa1dddf06c | |||
| f683f4544a | |||
| bc5b587651 | |||
| 8083031029 | |||
| 7b8102a42c | |||
| dbe2f5e4db | |||
| f27791b7de | |||
| 2a78170395 | |||
| cfca1c7b06 | |||
| fb7a67025f | |||
| 8a6cd48b8e | |||
| e21d1f539d | |||
| f62049559c | |||
| 1fe9dd03a3 | |||
| c3283a7297 | |||
| f423164a1c | |||
| 75fe596e24 | |||
| c5eb290e66 | |||
| d3b2c8246d | |||
| c11796af4b | |||
| 8591815d66 | |||
| b82870adb2 | |||
| 3c5b304b6b | |||
| f656698061 | |||
| d4b4bc5031 | |||
| 9bcf33b6d3 | |||
| fa550e8f03 | |||
| 2d73564eba | |||
| 1bd80247fd | |||
| 1c194e8163 | |||
| db7848c9ba | |||
| 97497ed9d5 | |||
| 60fcc652de | |||
| cb57717424 | |||
| 590f7786eb | |||
| 0024edcb7f | |||
| aa2d0d9a08 | |||
| fd126b8563 | |||
| bc97e7a5ea | |||
| 3dece4f46b | |||
| be05452c70 | |||
| 583650cf7d | |||
| 505915528f | |||
| ace8a787b4 | |||
| ed2ea9ac8e | |||
| 8d09a4abe6 | |||
| 1da959ab02 | |||
| 997dd9b88a | |||
| ebe66bdd6e | |||
| 12b573bc63 | |||
| cc8c163e0f | |||
| abc7f76679 | |||
| eee04895fe | |||
| f520b88f79 | |||
| f0f1c113e4 | |||
| 84a15761ad | |||
| 013fbb87a7 | |||
| 73764d23dc | |||
| 8f62703bf2 | |||
| 6dedae2e4d | |||
| d32131b2b8 | |||
| 0a790b2ae3 | |||
| ef1d5e3d76 | |||
| 30720bfdd7 | |||
| 234a18f016 | |||
| c525a19df5 | |||
| a987a31667 | |||
| 10329c3436 | |||
| 29f10bcd44 | |||
| 19fe9b8ac7 | |||
| 76da708352 | |||
| 145f01ff2d | |||
| 2c2d531e7f | |||
| 91e0f7fbc4 | |||
| bebeec7d84 | |||
| 18b1e00875 | |||
| 86d448c285 | |||
| 73f8867a6f | |||
| d021498fa9 | |||
| b83aa54661 | |||
| 429550ca3e | |||
| 2a6d8c2b1d | |||
| 01c9159830 | |||
| 3b3ed5159c | |||
| 636661dd45 | |||
| 0d7cb2bf25 | |||
| 991f590056 | |||
| 0e6758ccbc | |||
| 1d9c6520e5 | |||
| c81f11df0a | |||
| f39a1e70de | |||
| f0fa249d36 | |||
| 8f72197817 | |||
| 1556ac84da | |||
| b6da6bb835 | |||
| b1a882ea79 | |||
| 3305f2cc72 | |||
| c589a5211b | |||
| ff0d91979b | |||
| 9d6888cf74 | |||
| a4a7097c10 | |||
| cf2b058e7c | |||
| 048d7a9b63 | |||
| d6e37d0288 | |||
| 3cd562fa96 | |||
| bd569cb041 | |||
| e3c6a0e1a0 | |||
| b29176507c | |||
| db25e9719b | |||
| 661901b00d | |||
| b9352cfcc1 | |||
| e381b1901e | |||
| ebdff505eb | |||
| f806e4342e | |||
| 7f32d7d320 | |||
| b1b4b21d45 | |||
| 486e8b5445 | |||
| 9fe0e1e85f | |||
| 8d81240c58 | |||
| bdadcd4532 | |||
| 1de9a24677 | |||
| 6335f14a34 | |||
| da2ef381ac | |||
| 7b173f4c74 | |||
| d49eb0bbc4 | |||
| 91556d5bcd | |||
| 039abe1f75 | |||
| bc32faf467 | |||
| 74a4dfeb67 | |||
| f120533fad | |||
| a1baf39299 | |||
| 9f0f1bcc68 | |||
| 246963e181 | |||
| e3134ab0de | |||
| b38d52da72 | |||
| 411c4f40d9 | |||
| 694a85b652 | |||
| c84e72f53a | |||
| cb7e1a9d82 | |||
| 3c9dfc195e | |||
| a5c13041c6 | |||
| d699e98346 | |||
| 135a76febd | |||
| fae2856e7e | |||
| 98426b6186 | |||
| a63d9d2ccd | |||
| c567139e28 | |||
| 3d495b0753 | |||
| 4946b55c21 | |||
| ad7d7154f4 | |||
| e5b6a9f8cb | |||
| 04f27d36ef | |||
| 683fc98fdc | |||
| c303e83444 | |||
| ae5a40d686 | |||
| 409b7068bb | |||
| f2a12c7154 | |||
| efac2eac07 | |||
| f3f6f3e39a | |||
| f905586dbd | |||
| f393cea2c2 | |||
| e937998b40 | |||
| f85ec08886 | |||
| 7ff68f3844 | |||
| 04529bd524 | |||
| 2c681d93d9 | |||
| 1451fcb040 | |||
| 4986f3c2ca | |||
| e9edb85a32 | |||
| dca60ae4ae | |||
| 025f964b0b | |||
| ff46a8fa9e | |||
| 59412aba51 | |||
| a351ee9f76 | |||
| a5b14092cd | |||
| 90f1de9c3f | |||
| a516f06946 | |||
| 7c1f3e4519 | |||
| 3bb1a6336b | |||
| 7dc926596f | |||
| e775515c38 | |||
| c116f2b1bc | |||
| f24993d6aa | |||
| f053cf1fdb | |||
| 251318eaf0 | |||
| a66e7d79ad | |||
| 38884083a2 | |||
| 0b1088d9a8 | |||
| 875f9ca388 | |||
| 25cd1f25f1 | |||
| 7152ece70a | |||
| 896c76ce9d | |||
| 21d3dd4506 | |||
| 300b32c8d7 | |||
| 19e3b35fc7 | |||
| 320811f9ed | |||
| 991457496f | |||
| 1cb6134758 | |||
| a255b8e450 | |||
| 324f036b35 | |||
| 479a284e10 | |||
| e0660ce01c | |||
| 3a3f6cb7ca | |||
| 9797e6021b | |||
| a9212d33b1 | |||
| d32d3fd864 | |||
| a19cdd06cf | |||
| f4d0f89fda | |||
| ca34cb951e | |||
| 4c2d1b0385 | |||
| 6b4fefc123 | |||
| 86913dccb0 | |||
| 30101a4428 | |||
| 668e8f6f24 | |||
| 2f51e206c7 | |||
| 78b8b36a87 | |||
| af3ef86d19 | |||
| 50febaf477 | |||
| 907f317b04 | |||
| 6edb78d015 | |||
| b58846ab6e | |||
| 32233a2c7b | |||
| 8b2752441d | |||
| e6af29df94 | |||
| 4ae5404fe4 | |||
| 7f0bdc8ddb | |||
| 39836f115b | |||
| 4a699fe6a7 | |||
| 7cc61a887c | |||
| 8f73f1ed3c | |||
| 67fb027d39 | |||
| c6b61ea0ea | |||
| 32841234a7 | |||
| 6f6520ed0f | |||
| b3860f3754 | |||
| 0958917317 | |||
| f42fa7e791 | |||
| 7022d1f3bf | |||
| b592c41f96 | |||
| 62188571d7 | |||
| 8cca3392f6 | |||
| 7082516664 | |||
| 77a1fc9e60 | |||
| e2b1dd6532 | |||
| 7b7fdb0a65 | |||
| 414279f644 | |||
| be91c8580d | |||
| befaa782f6 | |||
| 8e29dae36a | |||
| d2438d10a6 | |||
| b3752bb2c6 | |||
| 02e145b0de | |||
| 0fcf8777a1 | |||
| 26cae5543b | |||
| b0573dec77 | |||
| c5f4e762e5 | |||
| f4c08477d0 | |||
| 4342ee6d4a | |||
| 7b73561923 | |||
| 87c0cf233c | |||
| 145cd7894b | |||
| 6cf8a76c29 | |||
| 7ea09ebe4a | |||
| 1dcb16365b | |||
| 83b33d9d7a | |||
| 83879fa945 | |||
| c3903f2796 | |||
| 18564553ad | |||
| fe3e87a9d6 | |||
| 78c734d2e9 | |||
| 5afa16d454 | |||
| 57233416d9 | |||
| f56ce29210 | |||
| 149f59e9be | |||
| 761892d6b1 | |||
| 4b639d5f9a | |||
| da8ed77aae | |||
| 12f46909f7 | |||
| faf18b1996 | |||
| 218deddd00 | |||
| d6fe650c4c | |||
| 919189db1a | |||
| 951191d99a | |||
| f814a96eac | |||
| 2654f4bf0f | |||
| 218aa423c3 | |||
| 195f3a7550 | |||
| 1c9343ed8f | |||
| 5c7d9981f8 | |||
| 63bc17a6b4 | |||
| 629490c4ae | |||
| d59d62f96a | |||
| 8a460c2368 | |||
| 6f60183316 | |||
| a21c62519b | |||
| 1b8f6d1739 | |||
| e087bce61a | |||
| d2f24c3e87 | |||
| 5d606bba66 | |||
| 864fe459b7 | |||
| ea8362ed63 | |||
| 3a1508c2ab | |||
| f914391aaf | |||
| 8992438aa6 | |||
| 197d5ebb44 | |||
| 4039498eee | |||
| a686231a5b | |||
| 97cf4bff1f | |||
| cc8e8434ec | |||
| 11727833a2 | |||
| df38fde336 | |||
| 00233d610b | |||
| 1b94b3c4de | |||
| d1c9030fac | |||
| 70071eef41 | |||
| 65dd56f53a | |||
| c8fb4af369 | |||
| 9e1ba992e3 | |||
| bad09fed44 | |||
| 5bd146bb85 | |||
| 75703f273f | |||
| ca7b49d209 | |||
| 379322db0d | |||
| 30bce8a682 | |||
| f413f0ee5f | |||
| be6778a931 | |||
| 84637c6ebd | |||
| 2ed6e9ba2f | |||
| 2e3cb54d38 | |||
| 0efac7eae0 | |||
| dc56f05d03 | |||
| 4d2625278c | |||
| bff8a947a1 | |||
| 9cb7406ebd | |||
| 38681ca6ca | |||
| 916696c906 | |||
| 1b3c4c935e | |||
| bf6d1f6555 | |||
| 9faeac4c83 | |||
| a863cf3d72 | |||
| 66417e6742 | |||
| 6ad4d6dd62 | |||
| 59607f8dbf | |||
| 62380a48d5 | |||
| 4245372495 | |||
| 964a448470 | |||
| 2151f28d4c | |||
| 45a92bcf9c | |||
| 0a0441756b | |||
| c1de2ebbf9 | |||
| 060549656e | |||
| 557c2db955 | |||
| ed8d064a13 | |||
| 7dabf507a2 | |||
| d69afa47a1 | |||
| 275a8175aa | |||
| 40bebf4cbd | |||
| b8bc323c03 | |||
| c6d32ea2b0 | |||
| 2ace35ad6b | |||
| b5ea8d3a78 | |||
| f3a05f6ed0 | |||
| 43eae4929b | |||
| 6144962c24 | |||
| 544cc36006 | |||
| 1834e6688f | |||
| e1ee56aa43 | |||
| a1779719be | |||
| 0e464af627 | |||
| fd7f0c3b5a | |||
| ed210a4fb1 | |||
| c2a0504980 | |||
| b6708871d3 | |||
| 275ea6aacb | |||
| f97d413603 | |||
| 2c7ea0606b | |||
| d7a7100912 | |||
| 2c54b8d77e | |||
| b30f278e03 | |||
| b642030a34 | |||
| 725976d472 | |||
| 80d87e1bf1 | |||
| d01f527e71 | |||
| 3e68c82171 | |||
| 51bde23207 | |||
| 934ed37fdc | |||
| 4a965f5408 | |||
| 6343da33c3 | |||
| e6edee863f | |||
| 9a1d62438d | |||
| d2ba3039c7 | |||
| b6ad4c10bc | |||
| 93954d314e | |||
| 282f85f1dd | |||
| 223d37ffce | |||
| 3d20388ca0 | |||
| 198c9d934e | |||
| d43005d91e | |||
| 02264b4572 | |||
| add652f18e | |||
| 1b9146b9e7 | |||
| 5178819b51 | |||
| f57c25ec27 | |||
| 794429b68b | |||
| 983a04bb00 | |||
| adbef16b9d | |||
| 157ea49328 | |||
| 17386e7aae | |||
| cb19cd673f | |||
| 4f0a297cf3 | |||
| 6553e331cd | |||
| 21908aea6c | |||
| 7c40798ee0 | |||
| 8cdc635cad | |||
| 7f5ac072e6 | |||
| d69af72c7a | |||
| ece1e202de | |||
| 91f38a362d | |||
| 5a3cc314be | |||
| 3dfaafd177 | |||
| bdba61975b | |||
| 3b9023ec2b | |||
| 4dfc7958b6 | |||
| 2fad318726 | |||
| 480b0e64a6 | |||
| 6ec7b5d404 | |||
| 0781d78da8 | |||
| 513a256ec1 | |||
| 9372790666 | |||
| a6532b7881 | |||
| cea3582ed1 | |||
| 6bd22a3e9c | |||
| 7b93b99054 | |||
| c6eb1525b5 | |||
| a4b8ba0bb3 | |||
| 02216b15e5 | |||
| 42efdf1e0a | |||
| 465f9e634e | |||
| 7e92f0e5c8 | |||
| 859a0d8db2 | |||
| 71740cabb5 | |||
| 8f77680750 | |||
| 509e4b337d | |||
| 942ff0c9fd | |||
| 24c3dd1f1a | |||
| 4f58e9945b | |||
| 547ded9155 | |||
| 4f112e8379 | |||
| 4d63f8ed04 | |||
| 944d39c836 | |||
| 433977b918 | |||
| d9796e3bec | |||
| 0a7b9109f0 | |||
| 89bf9ff65b | |||
| 7f6e223c0c | |||
| c696e5238b | |||
| d303fd0c7c | |||
| e1ad2f8a21 | |||
| 7053cf0182 | |||
| e25158975b | |||
| 7e028a82fc | |||
| 17fe3e4dc1 | |||
| 4bd09c45a0 | |||
| 6a7a255081 | |||
| 6701fdd486 | |||
| ddce14b20b | |||
| f1317e824b | |||
| db285af0b5 | |||
| 0434bf5a48 | |||
| 78d9111646 | |||
| 0f28a89c52 | |||
| 92db6599d8 | |||
| 70fb5dcaa4 | |||
| a265574da1 | |||
| 9911766435 | |||
| fb08ef9a9b | |||
| 2fab06111c | |||
| 11e3b1ab53 | |||
| 3c78f7dbe1 | |||
| 999cebc304 | |||
| b2e154377a | |||
| d5c68139c0 | |||
| cbde77a5cd | |||
| 8120041ba7 | |||
| 68bc8edaae | |||
| 7ec339985a | |||
| 70c0abaef8 | |||
| d4dcac93b1 | |||
| 43889cfb31 | |||
| 9e4e14802d | |||
| 9bebb22746 | |||
| 3b06b0ffc1 | |||
| 1b24d55b24 | |||
| c8c6444f6a | |||
| 45a88f0517 | |||
| 53cb3ca79b | |||
| 68526284f1 | |||
| 68cebc7ff9 | |||
| 38286b74e3 | |||
| 86f56082f0 | |||
| e87bbfc535 | |||
| 758e12d6dd | |||
| bff461081a | |||
| 33d36395aa | |||
| e373508211 | |||
| 9051edad37 | |||
| 678b268008 | |||
| 0361bcf94f | |||
| b1f02d30c1 | |||
| 2af0e5b176 | |||
| c204812d9c | |||
| 3b7def880f | |||
| e5ec2f03c2 | |||
| a1b3e8055f | |||
| 1e503261f2 | |||
| 9107a3e569 | |||
| c6070519ed | |||
| 30ece1be70 | |||
| b66a1d30a0 | |||
| 51e1f56873 | |||
| 86304fd037 | |||
| 04387e78cc | |||
| 2bfc44b947 | |||
| 33941eb37b | |||
| 0a45559276 | |||
| 800441e0ed | |||
| 95164d08d5 | |||
| 98d955ef1f | |||
| 950dadc14e | |||
| 31d2f0135b | |||
| c02928f294 | |||
| 951fff45e6 | |||
| 4fdd817ff5 | |||
| acba31bd6d | |||
| b5eea01848 | |||
| 074e02ccf2 | |||
| 4b9bc67cb6 | |||
| 936ef4116b | |||
| 9883d6851a | |||
| 4c08e126ca | |||
| bc53f8fdec | |||
| 0b76d3d7bd | |||
| abaf71418e | |||
| c96a906b39 | |||
| da96765020 | |||
| f654c8a892 | |||
| 336fce55df | |||
| d11946d86b | |||
| 3a4c72ac08 | |||
| 6d3f0f653b | |||
| 81d3534569 | |||
| c54922dba3 | |||
| 9da1f7b8d5 | |||
| a4ed3d97fc | |||
| 656694ee00 | |||
| c6b5936f8a | |||
| 03752ab60c | |||
| 7203542cfd | |||
| 4b36bbc122 | |||
| ecaf21ceb0 | |||
| 67fe4e1460 | |||
| a94503ad03 | |||
| ce6dd8688c | |||
| 1151bdc6db | |||
| ed223d1d76 | |||
| 650eee7705 | |||
| 4510eb6540 | |||
| 9a236f317d | |||
| 25c467d608 | |||
| c2daf0d74e | |||
| fa19616ad1 | |||
| 02cbd33284 | |||
| 941ae18d74 | |||
| 90f400abe1 | |||
| ff2d93d421 | |||
| 8d26bd9a17 | |||
| a9fa0484ff | |||
| d3d12ab62f | |||
| 1e29b1a31d | |||
| 9318bf5f2f | |||
| 6b35302442 | |||
| 2937e58215 | |||
| d42589b6cc | |||
| 26e9dfb4fb | |||
| f27d03a6bc | |||
| b1e3150a81 | |||
| 5d52053caa | |||
| ce668d051c | |||
| e06579ecf5 | |||
| 6c30af245c | |||
| c9c40a6dde | |||
| e748ac3d00 | |||
| aec79f3a79 | |||
| bf92cb1522 | |||
| 14e1920ff5 | |||
| c95cdf5a11 | |||
| c14d0616ea | |||
| 0112701145 | |||
| cb69515be9 | |||
| 3cd791e08f | |||
| 6e233e860e | |||
| b4f0ea441b | |||
| 39974d3a61 | |||
| a998006842 | |||
| c4e449fc45 | |||
| 765fbe2182 | |||
| 08dfa73b57 | |||
| a58e7a34e7 | |||
| 7a481beec6 | |||
| d51fad2de4 | |||
| c66755a756 | |||
| 886ad03505 | |||
| ba33ef0a68 | |||
| fe97dc3ece | |||
| 76c4875088 | |||
| 04a3aaee35 | |||
| fef03cda9b | |||
| 3292fde41b | |||
| 38cf25ac5a | |||
| 13d5d2f958 | |||
| 7f6b66c824 | |||
| 62c344b633 | |||
| 75ce2729f9 | |||
| 6669554867 | |||
| d3294da37c | |||
| 9b56bf25cf | |||
| e1a33d8a7b | |||
| 47a1224c13 | |||
| 5c57d81e94 | |||
| edefd3ec88 | |||
| f15098efde | |||
| 8ee99a0616 | |||
| 3ace1d04cd | |||
| 365bb772bc | |||
| 5ee6ada973 | |||
| ee0fa0e687 | |||
| 0d41f6aafc | |||
| 91b6499815 | |||
| 7cd1166a47 | |||
| f76cb677ff | |||
| 05e7f4e6f7 | |||
| 6684574bdf | |||
| 36a945f8e2 | |||
| 6a3d322033 | |||
| 00c003ec65 | |||
| f4d335c161 | |||
| 659f42139b | |||
| 0e791ed022 | |||
| 48655aa1a3 | |||
| 83fa80cfda | |||
| cf5b5ee085 | |||
| 429a4e3526 | |||
| d66d4c1cd9 | |||
| 7a1bbdf2dd | |||
| 29c1459568 | |||
| efad46a8a4 | |||
| a69c621305 | |||
| ad6dde6f26 | |||
| 2627e46723 | |||
| 408d70b55e | |||
| 3f369e528b | |||
| 312976294b | |||
| 77f42c479b | |||
| d60bd22674 | |||
| 2e67f77d3e | |||
| 6d8e8e6bd7 | |||
| 9c01945a05 | |||
| 7ce5ddd380 | |||
| 2b5de914f5 | |||
| 18a2426707 | |||
| 367fac6d54 | |||
| 157cc9e5eb | |||
| 81daf12598 | |||
| 9249b0652f | |||
| ee4c6b6265 | |||
| 68deab4a68 | |||
| c9c765b5b8 | |||
| 616f73d8c6 | |||
| 208c371afb | |||
| 3a59cfa9c0 | |||
| cf94527bd5 | |||
| fa93479863 | |||
| 8bc0ef8c27 | |||
| bd403b6d87 | |||
| 57a7328065 | |||
| 4945463beb | |||
| dfafa791f2 | |||
| 5f2cb6b3a4 | |||
| 5398fac348 | |||
| b217f6aa81 | |||
| ec597bea93 | |||
| 7a5c54fef7 | |||
| 4064f18de2 | |||
| 6d13457172 | |||
| f39518ef93 | |||
| 4b1cecd246 | |||
| 352509fd3a | |||
| d0f08f8839 | |||
| efd38a3471 | |||
| a4e74fea94 | |||
| fdb33b6189 | |||
| dcbb67838b | |||
| 1727d636a3 | |||
| 9eadc7f868 | |||
| 620118af5f | |||
| 3645764f9a | |||
| 769bfeb10f | |||
| 5fbaa9cfa7 | |||
| 007508ba12 | |||
| 0f1f18b232 | |||
| d6b754b133 | |||
| 1b80c83676 | |||
| ec4dc582b6 | |||
| 65646ff9e2 | |||
| 92f6ec918b | |||
| 62bd41d2e6 | |||
| 9d864ffd60 | |||
| c45b38cece | |||
| 0d7aee2c36 | |||
| be345a523f | |||
| 470bdf8741 | |||
| 59319fb55b | |||
| fb7695fdbc | |||
| 25b7552683 | |||
| 21d520378f | |||
| 9cd6607520 | |||
| efd3550f53 | |||
| 76402ec8d7 | |||
| f689142806 | |||
| fd563bda6a | |||
| 09a8f7122c | |||
| 608fb00844 | |||
| 5c45e9c306 | |||
| 950221dc13 | |||
| f816679596 | |||
| 80ccf18b16 | |||
| c7abd9062a | |||
| 4287f2229b | |||
| 8408055137 | |||
| cc0965d703 | |||
| 94b3d9d3e1 | |||
| 772bf7d6ff | |||
| 15c2e4bb07 | |||
| 419693023f | |||
| 2d081f2c19 | |||
| c76ce1fd85 | |||
| f38b4d37e6 | |||
| 73c92dfc57 | |||
| 61c5430deb | |||
| 21e4c597d9 | |||
| 4dbeee8cb3 | |||
| adc76c636e | |||
| 0dbf89b2b4 | |||
| 83241ac17d | |||
| 6aa5d39357 | |||
| 1304ecbe03 | |||
| aafc027812 | |||
| d84e0b166b | |||
| d1d46009cd | |||
| 3a4b6f0ea0 | |||
| b3d10ace21 | |||
| c17df7a6f7 | |||
| 1c13f5026e | |||
| b9cfede888 | |||
| 49fd9e90a0 | |||
| e09038232e | |||
| 2cfe310e89 | |||
| 973c7467e8 | |||
| 583df7ed7d | |||
| 6d05376f04 | |||
| e1f832bfa7 | |||
| b8092cd00b | |||
| 3c1dca6cef | |||
| c0f7dd6fe9 | |||
| 6af6e99480 | |||
| c5cbe48668 | |||
| 15707956ef | |||
| 4668fc87a1 | |||
| 468fb2cc41 | |||
| 7c79e7e836 | |||
| 925c6ffc3e | |||
| 0bf1f48623 | |||
| ffcb1c2513 | |||
| f286eb4d11 | |||
| 9346c83dc1 | |||
| a76267f5b0 | |||
| 1d3a7b3d52 | |||
| f78f04d553 | |||
| 7b6dabbe9c | |||
| ed01b3b8cf | |||
| 7880a30e57 | |||
| 3a3ff93450 | |||
| 3a1cdd37a3 | |||
| 8db38f8e75 | |||
| ff24ef4ee5 | |||
| 3faeec4add | |||
| 7d56ee5084 | |||
| b2afaabb8c | |||
| 3efaf90bc8 | |||
| 0c52887688 | |||
| 8aa1c1545e | |||
| 7c84f421c5 | |||
| 42a1dea7ad | |||
| d5e9155a33 | |||
| 5def5ab074 | |||
| 1b242e636b | |||
| 05f05c889a | |||
| 1367e285c8 | |||
| 45ec3e0bb9 | |||
| dc38f78da2 | |||
| 1b6a74fd93 | |||
| 9d8a1494aa | |||
| 08465cf236 | |||
| 7016848401 | |||
| bdd2a9e7e8 | |||
| 80256e6782 | |||
| 7907ef44f8 | |||
| 3a97a24686 | |||
| 7f208ed44e | |||
| 22e6cfaebb | |||
| 9d6f873048 | |||
| d526229a0f | |||
| aac68290ac | |||
| bd9a2c13eb | |||
| e5c65d53f8 | |||
| 121e9d0225 | |||
| c12a3b6610 | |||
| 43fee73924 | |||
| b72e9cb36c | |||
| 77d0a76186 | |||
| e89528315d | |||
| c34ccc9d53 | |||
| e51ba795f3 | |||
| 737dcc1d29 | |||
| dba08d230e | |||
| 15fb363874 | |||
| cbe2965849 | |||
| 59bfc45856 | |||
| ceb4581f91 | |||
| 07cc93cca2 | |||
| 1205178e26 | |||
| 8217c0f05f | |||
| c5c27b3cb0 | |||
| 04bbfae08e | |||
| b3efa73eda | |||
| f3efac059c | |||
| 9fb4ed2ec0 | |||
| f19013143a | |||
| ea3ee9bea5 | |||
| ccca6f4b6d | |||
| 6a583d2ba6 | |||
| 4049a32871 | |||
| 331c9ce1ff | |||
| 81ab2aca37 | |||
| 564b8276bf | |||
| b4a93d2dc3 | |||
| 260040b919 | |||
| 8dbef8b68e | |||
| 458b2d422d | |||
| ee51357dbc | |||
| fa679e873d | |||
| ed3fded8e8 | |||
| 92df82bfa9 | |||
| 0dc9c27651 | |||
| f6f54c35a3 | |||
| 0a9959bffb | |||
| b3a16cb852 | |||
| 9beb259333 | |||
| 63c57e8e02 | |||
| 0448a7ea68 | |||
| 5bd005b28a | |||
| 3aec6367d1 | |||
| cea3831c20 | |||
| 18ccceca2d | |||
| fffcdcb514 | |||
| efadf374d6 | |||
| 55ecb40190 | |||
| 01f6b3dfc6 | |||
| 786590eadc | |||
| c9174188ba | |||
| 64fb79e0be | |||
| 088ff5d0aa | |||
| 99e58b0297 | |||
| f4d1c5c006 | |||
| 72fd1e4e7c | |||
| f44e0a8e12 | |||
| 9338d9c2a6 | |||
| 75fc25feb5 | |||
| 5919874f6f | |||
| 213bb9dba2 | |||
| 3a9dc37d02 | |||
| 423c8a886d | |||
| f8a1e98de1 | |||
| 5487cf2070 | |||
| e998be3a9b | |||
| d70767ef3a | |||
| fbb355c5c9 | |||
| 20bc8071fc | |||
| 0438c6c51c | |||
| b39abba41e | |||
| 3ec8233a2d | |||
| 8ed51c806e | |||
| 57135a898f | |||
| 0d3d27a519 | |||
| cf42ad83da | |||
| e7bcb61a3b | |||
| 883b83f1da | |||
| 48977e6eaa | |||
| efe2488155 | |||
| 29c04b6f9c | |||
| 984b6234d2 | |||
| dac4a5452d | |||
| 5f9e82204a | |||
| c4142d93c3 | |||
| b34a2c7ee2 | |||
| cd7cc1b71f | |||
| 4c6dd564a4 | |||
| 28e46a82ea | |||
| 10e294784e | |||
| 2da725340c | |||
| 882d3a765d | |||
| e52e2f10bf | |||
| dfc19e79f1 | |||
| f59bd3da7a | |||
| 50791e3aa7 | |||
| 8211b2358f | |||
| f2e1f3393d | |||
| 0ffec0a32d | |||
| 1e5e705458 | |||
| f2af6ea60d | |||
| de9187fee2 | |||
| 5eed091185 | |||
| 06644b5748 | |||
| bb853f65e0 | |||
| eb830dd014 | |||
| de82d1e90c | |||
| 53e838083c | |||
| 975368de8f | |||
| 89173be055 | |||
| fe2bdd027e | |||
| b376a7c399 | |||
| 2df262d877 | |||
| 320ab050fe | |||
| 1816d7aa4c | |||
| 41b763f331 | |||
| 36db57615d | |||
| 8f7ed1dc15 | |||
| 83a8a0cf21 | |||
| ffb0e27efa | |||
| e71c4b3bc4 | |||
| 85a0adb004 | |||
| f1475cd3d7 | |||
| 8c14812537 | |||
| 27aedf0563 | |||
| 95c2c1643e | |||
| f952f6742f | |||
| f3a10a8166 | |||
| 0790201cca | |||
| 5938c49453 | |||
| 14fb080f80 | |||
| 034b8db070 | |||
| d3ce0cb82f | |||
| 4dbda8dffd | |||
| 01f32e0f45 | |||
| 9a0de545b8 | |||
| 86c530e967 | |||
| 049b769f68 | |||
| dcd6626fe6 | |||
| 601cefe975 | |||
| 1fc2ab7f7d | |||
| f2c5b2bd49 | |||
| f31f88ce31 | |||
| d35f5152a9 | |||
| d8e19db8bf | |||
| 376e56d5fd | |||
| 72f856eca4 | |||
| dbab75eae7 | |||
| 7457da80e9 | |||
| 443e01d38c | |||
| 880438c5c1 | |||
| 1984cf02cf | |||
| 5423d3ca61 | |||
| 3f448df1d3 | |||
| a626b44bbe | |||
| 4c6e2fca91 | |||
| ab4d9ae4bc | |||
| fb3d075da2 | |||
| 657e48de7e | |||
| 1b63cb1406 | |||
| 4bdabbfbe9 | |||
| 01f0dd4498 | |||
| f59650d8a6 | |||
| 0e444fd925 | |||
| 9b8b57d186 | |||
| ca6a52727c | |||
| 3dfde6bf6a | |||
| 780394b051 | |||
| 6942e3467b | |||
| 70eb8a7300 | |||
| 15a8c23cd0 | |||
| 49f0e368d0 | |||
| 590608a215 | |||
| 202fec2a35 | |||
| 817bfa35e5 | |||
| 110c9800f0 | |||
| 1a6dc973bb | |||
| 44dd674dab | |||
| 4a3ce640d7 | |||
| df6ebf83b4 | |||
| e5dcc5a407 | |||
| 1ee8abb0e6 | |||
| dd40435425 | |||
| 74cb57c761 | |||
| 86123f28f7 | |||
| f97ab32e7c | |||
| b0e2544e4b | |||
| 0d59963b53 | |||
| c669aafedb | |||
| 2a2a40af7a | |||
| 1df12d1677 | |||
| 14a2d7e860 | |||
| 3f2c05664f | |||
| 9b05d1d68e | |||
| 772d668389 | |||
| 03360a663e | |||
| e1e9f690c9 | |||
| 934e81d16c | |||
| 88bb31d3e6 | |||
| 33f5894547 | |||
| fa46d2bef8 | |||
| 65f8556ee9 | |||
| ebe174fbef | |||
| eaaeedbb37 | |||
| bf45c176a7 | |||
| 87a8e4c216 | |||
| 30cc7d4f0f | |||
| 4a47867e49 | |||
| 5fced642fa | |||
| 9fb559307b | |||
| 96c8c2b9c3 | |||
| 145cdf6985 | |||
| 5910fd95ff | |||
| c0dbf2df7f | |||
| cfaadef669 | |||
| eeffe208ec | |||
| 358f13500b | |||
| 016f16954a | |||
| 9dc61faa6f | |||
| 2173ab3437 | |||
| c1543545d2 | |||
| 5da936d96a | |||
| 0dead73837 | |||
| 66a6dd1f0c | |||
| 8a8109272a | |||
| 7ea30c449e | |||
| a6e4096773 | |||
| c1e2d646b6 | |||
| 710ac6847d | |||
| f0267eae36 | |||
| 1632ee3537 | |||
| a16cdb948c | |||
| c4ae27dae6 | |||
| 053bc49738 | |||
| 3a1de9fbdc | |||
| efcaadd0b4 | |||
| 0170cb066d | |||
| 6bba5ca25a | |||
| edcdeb31ea | |||
| 1286007b2e | |||
| 9faab093f7 | |||
| 64bf145e4b | |||
| 733008cfc4 | |||
| bab4582139 | |||
| fddf2843b4 | |||
| f8d83f8273 | |||
| cfeaf188ed | |||
| 58ad1ecbfe | |||
| 463538178d | |||
| 14907065d7 | |||
| ce2059a4b9 | |||
| 2bfc157e64 | |||
| fda7a2cf13 | |||
| e69de8c26f | |||
| f404c80714 | |||
| 92ca2386ea | |||
| 59b25d6837 | |||
| a6f7936311 | |||
| e2b680c223 | |||
| bdaf2e3b4f | |||
| 2190022e64 | |||
| e000e2b9fd | |||
| 7392b4de17 | |||
| 79b0a5fada | |||
| aee9442e52 | |||
| d5000820fd | |||
| 569d5d1fce | |||
| 9d91d197e4 | |||
| 5b767ae948 | |||
| 6ea8003df2 | |||
| c8ab82010a | |||
| bf1bec9c6c | |||
| e0c90ec9e3 | |||
| 7ad5021147 | |||
| fd73c3fb3a | |||
| e3dbf7cc41 | |||
| 18749c580e | |||
| 396db30fbf | |||
| 6b38868de6 | |||
| 01a46ad880 | |||
| 46f8251e94 | |||
| 77f882f45a | |||
| 8c72fd104e | |||
| 549656884b | |||
| 5b8b0a8aa3 | |||
| b1924d4db6 | |||
| 1b877118ef | |||
| 682a5daf1c | |||
| fcbfaac1fd | |||
| 3787b6f1c7 | |||
| 6e08835496 | |||
| 191695da5a | |||
| 2215087f96 | |||
| 32234ee7fc | |||
| aa37f697bf | |||
| 49448fafaa | |||
| 057303d57c | |||
| ccc85d98e2 | |||
| c30a8b5a29 | |||
| 295010893d | |||
| 7fb807919c | |||
| bd8f8ef28d | |||
| 3901a381cc | |||
| 12f6e51ef6 | |||
| aa8454e30a | |||
| 6b70230e0d | |||
| 5e0ba9971c | |||
| fa577c9475 | |||
| 11a958b8ca | |||
| 6952db6762 | |||
| 51898cffe8 | |||
| d8337d703d | |||
| adac0c353c | |||
| 04fca16420 | |||
| ca89b6e7a8 | |||
| ac1173c628 | |||
| 0a0ae111f6 | |||
| 71a6e015f4 | |||
| e8bbb8a1cc | |||
| 04764998cb | |||
| 5262d716e4 | |||
| 7addacba38 | |||
| 8f8c9c8ec0 | |||
| 3a9832a8c6 | |||
| 4a40c10d4c | |||
| 58f8ca7d66 | |||
| 4d950fec66 | |||
| b4f68f4fc6 | |||
| ac742aad70 | |||
| 53d225a1d1 | |||
| 549b0f9313 | |||
| 2ce106382a | |||
| b44f43e5db | |||
| 2321b9a04e | |||
| 3bd518cf7f | |||
| c57109c2f3 | |||
| 522640edd9 | |||
| 5fc0629201 | |||
| 26edd7431a | |||
| fd58957b06 | |||
| 12bb0b86dd | |||
| 165c1fc0b6 | |||
| 4116d89d5f | |||
| cc192efe45 | |||
| feef1a35b9 | |||
| 55a2f46604 | |||
| ed8b303400 | |||
| c785b10603 | |||
| 90512bdd5f | |||
| 4acd06eaba | |||
| 10751e9a6d | |||
| d2ebc58c3c | |||
| d51c5a2d68 | |||
| 1f24845431 | |||
| 3b02b62ba5 | |||
| 24ae787736 | |||
| cd735ef459 | |||
| 180fea8ace | |||
| 5f02c4b5ad | |||
| 41680f6089 | |||
| 730f7d3dff | |||
| d32033f105 | |||
| 440274d639 | |||
| f93130a8a7 | |||
| 3d9bddfb9f | |||
| 439abbcce9 | |||
| ac91367801 | |||
| 2a63cc474c | |||
| 56261263f5 | |||
| 04b57bbe9d | |||
| c550f83a04 | |||
| 5224ef4b1f | |||
| 2ab033e76e | |||
| fa2e669eda | |||
| f0ba1f2ac0 | |||
| 6d0237ec71 | |||
| 97dff4640a | |||
| 00b571a429 | |||
| 86e0f49231 | |||
| f2f205f9bd | |||
| f84ec090cb | |||
| d37ed9ff6f | |||
| f5a5f5e51a | |||
| fe010242d9 | |||
| 545ebf81bf | |||
| 408934932a | |||
| 6f42824c35 | |||
| c3215d51bd | |||
| e541b96a71 | |||
| 904a2f466e | |||
| bad48da11a | |||
| ce2d1d6e2b | |||
| 2820071db1 | |||
| 5937185ce9 | |||
| be9b7a0d24 | |||
| 7ca09ad749 | |||
| 686a7a40f9 | |||
| 2a7b2835b6 | |||
| 69ecf3b145 | |||
| 2cd748b50c | |||
| 291133beb9 | |||
| e10c17c866 | |||
| 0048cbef08 | |||
| d9d65309b3 | |||
| d5d8032b5b | |||
| 693c749da0 | |||
| 7218e31a9c | |||
| 1798f3921f | |||
| d12c56a623 | |||
| 26aa3d3ce7 | |||
| c97a87d1f6 | |||
| 9bc185d459 | |||
| 4c651c15ea | |||
| a98e6964ef | |||
| 6f8d9c4693 | |||
| fbc4bd0c96 | |||
| 03c9241783 | |||
| 3a983271d6 | |||
| 03fe4afe32 | |||
| 12627022d1 | |||
| fabfe16d45 | |||
| a34758f938 | |||
| 20f5c3ea28 | |||
| 62e490cfe4 | |||
| a9dba39623 | |||
| f1d417597c | |||
| 549f679bf1 | |||
| 6ba052dcc4 | |||
| de873b84f5 | |||
| 37558ac1b4 | |||
| 9140d5a091 | |||
| 7827af0d90 | |||
| 1af8d20adf | |||
| 91df096698 | |||
| e8fd0498a7 | |||
| f3073e120d | |||
| a571624e13 | |||
| 74b649c04c | |||
| 7973b99f50 | |||
| e8f5a8b89d | |||
| 2d0bda933c | |||
| 49588da73d | |||
| 3e2d845342 | |||
| e92d2bd70a | |||
| de1b545df1 | |||
| 3bec28b2ff | |||
| 8cad116dd7 | |||
| 35adb75d80 | |||
| e9908b1d97 | |||
| fffd2eb70a | |||
| 136b9c0f50 | |||
| 0f1206b4ee | |||
| 46d7e4c707 | |||
| c874783742 | |||
| bb296f50d9 | |||
| da68b53ff9 | |||
| bbe141d44e | |||
| 8a03e41a7c | |||
| a79e1bc976 | |||
| 056bfbf7a3 | |||
| e0b64a487d | |||
| d47d1d8f26 | |||
| 42a07de9a7 | |||
| aead855470 | |||
| 335b2314f1 | |||
| 89bab24c14 | |||
| 3a439dcdad | |||
| 20d82eb92f | |||
| 319e1d1191 | |||
| 5f3492dbf8 | |||
| 107c8c0b1f | |||
| 8c6d9586bf | |||
| 1271fc6bf3 | |||
| c9df03c40c | |||
| d8e8dddd25 | |||
| 27f6745123 | |||
| 964f448334 | |||
| 20ee03bb44 | |||
| 77bd677182 | |||
| e024d047e3 | |||
| 40943edc06 | |||
| e6699c5424 | |||
| bd8a307e50 | |||
| f71301cafc | |||
| 562bf9331b | |||
| 11e6eb94b5 | |||
| cee3aa2a7a | |||
| 81e3783488 | |||
| fc7f9786f8 | |||
| 0808c0edf1 | |||
| 8de6746efd | |||
| eb9b8ef7c6 | |||
| b09621b915 | |||
| 8d667f9367 | |||
| 56dfe6630f | |||
| 8b3b181a48 | |||
| c952768542 | |||
| 1a368aa996 | |||
| 61449458cf | |||
| 4eb547e535 | |||
| b54acffaef | |||
| 65a1833e1f | |||
| 1ce4f25811 | |||
| 3127105516 | |||
| d59ea4be78 | |||
| f256f04440 | |||
| b444aaa67e | |||
| 745185e689 | |||
| 2bfa891f0a | |||
| 147167bed3 | |||
| 565e18e8a3 | |||
| 55b4595bbf | |||
| eeb2c463dc | |||
| d9bb0e9a52 | |||
| 7d07ab7c7e | |||
| f8ff3aac58 | |||
| 299a7728d1 | |||
| 39dc6a1742 | |||
| f21c5aa7f2 | |||
| e9bc3f26a5 | |||
| 23eaddd6ea | |||
| 8143ce8450 | |||
| 0a487ec43e | |||
| 0edb483802 | |||
| 06a32ce0a1 | |||
| 8cae00407a | |||
| a57ec87c67 | |||
| 4e62491ea4 | |||
| 5758029c1e | |||
| 8f08710c58 | |||
| 90f98105f0 | |||
| 90354aa330 | |||
| aaabebe7f5 | |||
| 80a92dcdc2 | |||
| dc9081e9d4 | |||
| 3c299637b6 | |||
| 07af333943 | |||
| 0bbc781d0c | |||
| 79bf64f079 | |||
| ed67d39456 | |||
| 2f8cc75432 | |||
| 03cccef805 | |||
| 6d5a0c2718 | |||
| 42b359eb5c | |||
| 3071587f11 | |||
| f3ec9768bc | |||
| 23159807b0 | |||
| b1ba9f76b8 | |||
| 0e51dfed46 | |||
| 09b00335f8 | |||
| 3d274815d9 | |||
| 70d60b905d | |||
| 3e2ffb25a6 | |||
| 8b9bef5cb3 | |||
| 31e72efc91 | |||
| 60b7252597 | |||
| 3980b62df2 | |||
| b306df726a | |||
| 3d5a79be3b | |||
| ba78d1a9ae | |||
| 241811298f | |||
| 8a0ddc43ab | |||
| 898fa0e41b | |||
| 081ff4dec0 | |||
| 3c69b8511d | |||
| 6843d86ecf | |||
| 2e91200136 | |||
| 852304c417 | |||
| ee752e3885 | |||
| b9480e4302 | |||
| 2ae4d07971 | |||
| 90cac8a118 | |||
| db18274f6e | |||
| 172bad8b55 | |||
| dfe454e18f | |||
| 3d8dd29b4c | |||
| c3ff213ec9 | |||
| e80e5e1f8c | |||
| bba249d5ce | |||
| f57df2bee5 | |||
| b930638156 | |||
| 39c1de19fc | |||
| 17724fc8d3 | |||
| 4c6d11d9ed | |||
| 05d77a85c9 | |||
| e95a133cdd | |||
| c21382d721 | |||
| 8c15125e23 | |||
| 64ddbd97dd | |||
| 9c24bcb7a9 | |||
| 8f016726f0 | |||
| 649fe7a490 | |||
| 35f1cdf89c | |||
| 06adc34fb3 | |||
| 87bf07f95e | |||
| f05bf3f845 | |||
| ab512d087c | |||
| 6799c29921 | |||
| a3f1da1981 | |||
| 3b225651cc | |||
| a40d691159 | |||
| 4ebe60b2ad | |||
| 5a70859593 | |||
| c7be810e65 | |||
| 101217cfb6 | |||
| 5c2aa4677f | |||
| ab9bfa68ae | |||
| aa8c2ca277 | |||
| 84509087ac | |||
| 2450d461fd | |||
| 50c590ae26 | |||
| 516dff06ee | |||
| 9a8af05bfb | |||
| c9bf61c387 | |||
| 4f0f2e8c16 | |||
| 6f042a2142 | |||
| 91416bdbb2 | |||
| 9b093f7569 | |||
| b004d1602d | |||
| 6cca73b999 | |||
| fafd6df13e | |||
| 8f77870526 | |||
| eb0462e89b | |||
| 80748d7d85 | |||
| b694d53b73 | |||
| 8004e82c50 | |||
| 85b5849228 | |||
| 6b86777e96 | |||
| efe64a4817 | |||
| ad777f36b2 | |||
| de77ad867c | |||
| 9b35f86497 | |||
| 84fc8b1931 | |||
| 455c85fb69 | |||
| 2a2fed695b | |||
| c1f28bd410 | |||
| c74e0bb6b3 | |||
| 5b9e158035 | |||
| a8d200dd02 | |||
| 34ae967cb8 | |||
| a67f14825e | |||
| 50c14d0ab8 | |||
| 0edb6e6f6f | |||
| 36d0dacda1 | |||
| 83b74070aa | |||
| f80af68686 | |||
| fe4ac06f43 | |||
| eaaa3e980a | |||
| 07629bfb9a | |||
| 88fdeca2bf | |||
| a3c8eac38b | |||
| bd5380c0b4 | |||
| c3b5767999 | |||
| 9e5c2732c9 | |||
| b8957fa917 | |||
| 52c139dcdc | |||
| 39d4bf1494 | |||
| 4c713e3387 | |||
| bb486f5148 | |||
| 524fea1297 | |||
| e9528ebb98 | |||
| de18283c3b | |||
| cc1c7561a3 | |||
| 5d928f07a0 | |||
| 01b882480f | |||
| 21d52fdbdd | |||
| 7f8b9de560 | |||
| 761f22b63d | |||
| f9baff2a3a | |||
| 71eca4ffcc | |||
| bc8dca5105 | |||
| 3ae3dffff7 | |||
| 81c6023940 | |||
| 3a0f27fa7e | |||
| 60e339bac0 | |||
| ecb88f45b7 | |||
| 24a869d15b | |||
| 1d427a1ea8 | |||
| 90e25867ad | |||
| 2ae56e61cb | |||
| 56c0830328 | |||
| 093f139d34 | |||
| 1083efc212 | |||
| 69773c2619 | |||
| 2525b5a5d8 | |||
| b00804102d | |||
| 8d1d657c44 | |||
| ff9c84ff94 | |||
| 3aa2bf8a76 | |||
| 6cd09c6af2 | |||
| 46a8486245 | |||
| c5caf8f8f4 | |||
| a229ece693 | |||
| b435137332 | |||
| 2cdbc9f4db | |||
| aa6884e484 | |||
| 4f4d694687 | |||
| 1b47999e80 | |||
| 02427651dd | |||
| 8eeb088e50 | |||
| 3f62581556 | |||
| b53a7f6ee8 | |||
| 4356603665 | |||
| 1cae5e8b97 | |||
| a4591afba6 | |||
| 6ea4c77dd5 | |||
| dc3d90d696 | |||
| 00de919eb4 | |||
| fdacc2f7ab | |||
| 5a64c29228 | |||
| c5cddf1607 | |||
| 44c7844a4b | |||
| f293da5b34 | |||
| 4f044de79e | |||
| da116e6077 | |||
| 2489180c47 | |||
| 4ec4d330aa | |||
| c75ca1c2d6 | |||
| 67462e9fc4 | |||
| 424b6303ef | |||
| 59c4e2c354 | |||
| ecca8bc86e | |||
| 43ca920b10 | |||
| e1a3f8f053 | |||
| 6e7cb63e7d | |||
| ab1177d987 | |||
| ee0a1d281d | |||
| aef7b9a1dc | |||
| 7cb8de5b69 | |||
| 5c2fb1c42b | |||
| 553857583d | |||
| 6d0923153f | |||
| e34eb48914 | |||
| 05ab6ef3ab | |||
| 81eefc1377 | |||
| 895d854e1c | |||
| e432d4f808 | |||
| 56c0ae294b | |||
| 0b9d68b4f2 | |||
| e2e034f795 | |||
| bb5e3d51b8 | |||
| 70b23614b5 | |||
| 24a75e3765 | |||
| 07c2e34d87 | |||
| 5bcbe76f2c | |||
| d694ee3ef3 | |||
| efbdf4e1a8 | |||
| 44bfc2e846 | |||
| 0121bdbb75 | |||
| b8ba77a7b5 | |||
| 65dd5cc6ad | |||
| 8aeb994839 | |||
| 64daa444dd | |||
| 26aab4f38d | |||
| 6059df1b67 | |||
| 2a0c85c772 | |||
| 3488fbe64c | |||
| 811a98ad19 | |||
| 4462f4b90e | |||
| 4143a79f7b | |||
| 3ed9b00398 | |||
| b005b75331 | |||
| a9f9e2cf35 | |||
| 5602b94dcb | |||
| 930de640ac | |||
| 6d9fba8191 | |||
| 624c6f0a6e | |||
| 7d2f7fae45 | |||
| 3f917b39c9 | |||
| f1336a5ce7 | |||
| 7a10d504b2 | |||
| 831aec6488 | |||
| 6eb229ac1e | |||
| c58db665dd | |||
| e222fb1783 | |||
| 4c6fa89053 | |||
| 98815ffdf6 | |||
| 31a0192c2d | |||
| 53f8091e3a | |||
| 012cbf7995 | |||
| ac26c91cba | |||
| c13162aada | |||
| 9fb6eea8b7 | |||
| 23c4f19cda | |||
| 3b34570749 | |||
| 0412ca5810 | |||
| c80518bf3e | |||
| 61ee6eb8af | |||
| 654e8b41fa | |||
| 7080458f7e | |||
| 08d236f5ec | |||
| e332a7d113 | |||
| 7879709f62 | |||
| 56e030762e | |||
| bac73150ca | |||
| 2e1fb15ada | |||
| ae9bcd6f6c | |||
| c18c679b9b | |||
| d014ee0b72 | |||
| a30ef7250b | |||
| 6f6e7ea921 | |||
| 0c714ba4a1 | |||
| 5f539aacd9 | |||
| 570ce4f4b7 | |||
| 3c7c9048eb | |||
| 6a77df7b41 | |||
| 41243757ee | |||
| 2af311bd7d | |||
| 1bc9ee7110 | |||
| 4e040f8e77 | |||
| 26c1c6db3b | |||
| d38da83656 | |||
| 4a9a1b40e9 | |||
| dc971b9a59 | |||
| 58f163ed5c | |||
| c0c9f0122c | |||
| d33395e46d | |||
| b83c7d3929 | |||
| b5df016b1b | |||
| a8b6be3b38 | |||
| 78cf175f5a | |||
| 1b78856a7d | |||
| 8194287391 | |||
| 2eecea9a07 | |||
| 465032dd4f | |||
| e473315a89 | |||
| 9d34ad5287 | |||
| a532cc5cf9 | |||
| 60c6c5bc41 | |||
| 0cbbbe8503 | |||
| 47a8d3e50a | |||
| 304da09f3b | |||
| acd4dcb56e | |||
| c170456cde | |||
| 4e739e4b06 | |||
| 137c6919f6 | |||
| 842ce30190 | |||
| df1539040c | |||
| 2a04459bb2 | |||
| 6367bf7c75 | |||
| a0456dc430 | |||
| 52ec831b16 | |||
| 8263062fab | |||
| 9a2bf78a8e | |||
| cb16f7a60b | |||
| ad84631ddb | |||
| 95131c7658 | |||
| 936eef194a | |||
| 941d871daf | |||
| 609ee663fa | |||
| d78426d708 | |||
| a9543df6db | |||
| aae388be93 | |||
| 53804cac5c | |||
| 4ef970b4da | |||
| b199f133b3 | |||
| 7d1b183a1b | |||
| 49d119e92e | |||
| feed1da570 | |||
| 193ad9e09d | |||
| a3ad835d84 | |||
| f8afee8ebd | |||
| 7e955fc312 | |||
| eebf92366f | |||
| 3c23e166a7 | |||
| 26a8439ce4 | |||
| 2c2e8fa1ac | |||
| ddc2fa74b9 | |||
| 93d51b83c3 | |||
| f83eae4a46 | |||
| 87c6d11fca | |||
| e87ac86e48 | |||
| de8063a43a | |||
| a73dabcb67 | |||
| 7782e81101 | |||
| aa70687d9e | |||
| 38d32de06b | |||
| 7720c72b73 | |||
| ff9505073f | |||
| 21ee1c31a7 | |||
| fd01ba1fcf | |||
| fbf53524ed | |||
| 1f2a701ace | |||
| 0b87a573b3 | |||
| 74438716af | |||
| d10b348e74 | |||
| 68e9be47d9 | |||
| bddd03c2fd | |||
| e23ba50dd8 | |||
| 261ab7ae68 | |||
| 21c8c76dc3 | |||
| 266d0f9d05 | |||
| 69d25c1498 | |||
| 3cd2b3925a | |||
| 33e9eb371e | |||
| 07572d1e8d | |||
| dde4f558f3 | |||
| 79d2574ea7 | |||
| 51fb5c4a15 | |||
| 875c6b973b | |||
| 21e1312dd7 | |||
| a722ef3b03 | |||
| 80ba5d29f2 | |||
| a35e6a0f54 | |||
| bdc1958c08 | |||
| 3f2bac71c6 | |||
| 6b9a11b697 | |||
| f17ecba519 | |||
| ce0b014a5a | |||
| ad48d2997e | |||
| 1c1781ce76 | |||
| 5fd001354a | |||
| a18bdad44f | |||
| 600dff62e8 | |||
| db7a402e9b | |||
| 0e53f9052f | |||
| 62e69cacb7 | |||
| 852c88c341 | |||
| 455f52f1f5 | |||
| df6012c58d | |||
| f68a3dde46 | |||
| 25e6b1cac8 | |||
| 4ad20526db | |||
| 18cd017f58 | |||
| 0161664b6c | |||
| 25df31bf96 | |||
| 09438b440e | |||
| 6a5f5b249e | |||
| 3a20114c39 | |||
| f411d50253 | |||
| 00851df25c | |||
| 8822d255b3 | |||
| 4b4ba86167 | |||
| 7ea820f6e1 | |||
| 53d8cf0852 | |||
| 761806c678 | |||
| d6abd639f3 | |||
| 6078bbbe24 | |||
| c1c81df4de | |||
| ee8a4698a9 | |||
| c1956d3f05 | |||
| 56316dc5d9 | |||
| 405451d783 | |||
| b0275afac2 | |||
| ae71f41138 | |||
| ec2f07e1aa | |||
| b5c74b5666 | |||
| dc946dffbc | |||
| 937baadb9b | |||
| 8d0c03b4f0 | |||
| a3fba73044 | |||
| 116cf31199 | |||
| cdb78e4c75 | |||
| 0bb9c56e94 | |||
| 821f1c876b | |||
| e9b95f8567 | |||
| 103d811441 | |||
| bb4f5a3fa1 | |||
| f5cbdeac8f | |||
| 56062e8e4e | |||
| 03c85d48e5 | |||
| dd8f0fbdcb | |||
| fac61a76e9 | |||
| 3b09ab3ca1 | |||
| 16bfe79305 | |||
| d668f97c98 | |||
| 18bd10b03d | |||
| 6b1d089caf | |||
| af93401385 | |||
| fa5add3d99 | |||
| fb971580e0 | |||
| dcaea98e33 | |||
| b95079d1c5 | |||
| e59f36cdc0 | |||
| 0f2f041d8b | |||
| 33d2837ec3 | |||
| 7cede221de | |||
| fe47435fc7 | |||
| 491226a916 | |||
| deb7433453 | |||
| 14973a35c2 | |||
| b5779f8654 | |||
| f7d1984257 | |||
| 3fba683090 | |||
| 430da8ac09 | |||
| dcd9b5c382 | |||
| 348c293962 | |||
| 34309da10c | |||
| 6db973f430 | |||
| 9f27bafa62 | |||
| b05136146a | |||
| 13d3be637b | |||
| 44de7fad6f | |||
| b99243406b | |||
| 0e9ec811b0 | |||
| 6979177fb2 | |||
| 3aa8bfa6ca | |||
| 17b356b08e | |||
| f72ae490a8 | |||
| b0c3d0d2e3 | |||
| 9dc344999e | |||
| 663c096400 | |||
| 420b4d119d | |||
| 58b752c63b | |||
| 4740232fa4 | |||
| 0cc9994b8b | |||
| 1e78628b23 | |||
| 00f5ddc93c | |||
| f79f2105fd | |||
| 828c51467f | |||
| 963e271bce | |||
| 0ab41215f0 | |||
| d153c4da07 | |||
| 91aa783c3d | |||
| a54845bf76 | |||
| 8a56a5f1ed | |||
| f585c80491 | |||
| c495b12cef | |||
| 1d6f7f862f | |||
| 0a82c84006 | |||
| a614a02b90 | |||
| 0945e2c5c6 | |||
| b1b49413d0 | |||
| 01af303d63 | |||
| 751060305c | |||
| 389fcfaf3d | |||
| 3eb0c534a5 | |||
| 8a2f84b678 | |||
| dd00735409 | |||
| 6ba7e85e24 | |||
| 32814d1833 | |||
| 941d93c2f4 | |||
| c73e9cbc7c | |||
| ae89e4bf21 | |||
| d1e64d0cfb | |||
| 8f9f9590d9 | |||
| ed68093310 | |||
| 23655e748d | |||
| 98624871bd | |||
| 7b154c0834 | |||
| d9f056242f | |||
| a4268d288e | |||
| f90c91dded | |||
| 5c8890c3c1 | |||
| cd3c6809a9 | |||
| e2c17528c2 | |||
| e00f565f37 | |||
| 085e797c30 | |||
| d753db590b | |||
| eab074a27b | |||
| e2a3e3816f | |||
| 01dd57adab | |||
| 08e674b695 | |||
| 9f70970e61 | |||
| e9ffd5a125 | |||
| 20f4469361 | |||
| feac096dc2 | |||
| 67985d449a | |||
| a6de59c198 | |||
| 01eeb98e35 | |||
| 49a7defbf0 | |||
| c1ba5de686 | |||
| 81428f23d1 | |||
| 8af86bb746 | |||
| 8513f5c413 | |||
| eadec35093 | |||
| d6dbd621b8 | |||
| 7168f76614 | |||
| 1cda95f23c | |||
| bb1cd2bbce | |||
| 3f90ac5712 | |||
| 8d249a843c | |||
| 858b41d835 | |||
| 61aea05af0 | |||
| e7c764d5f5 | |||
| 09a9afe4e7 | |||
| 5a26503da7 | |||
| 5faf5ea1f8 | |||
| 0754c29c22 | |||
| d5c6dcf111 | |||
| 6a57ddd33c | |||
| bd711cdc1f | |||
| e669e493c9 | |||
| 48f290196c | |||
| f8985dbb39 | |||
| ef594d52e4 | |||
| 23bbb2f8c6 | |||
| 42f181cc7b | |||
| b3d2d39b60 | |||
| e323d917a4 | |||
| 73c7733ebc | |||
| 87f7f9443e | |||
| af6bbbc59b | |||
| 5b35a364a9 | |||
| d56ebadbc4 | |||
| 04accdeddc | |||
| 70575f9e33 | |||
| 8e16586d84 | |||
| 6920dfb800 | |||
| 02d93770aa | |||
| cd124231c5 | |||
| cd75848882 | |||
| 1bae15ede9 | |||
| 8c2001adbf | |||
| 79ca235e7c | |||
| 4570fcaa8a | |||
| 90670cf1be | |||
| cc86f427d2 | |||
| 2144791d52 | |||
| 33aabf44e7 | |||
| fc1ea27380 | |||
| 81946294d8 | |||
| 77270fa78c | |||
| 9e29289dcc | |||
| 6198943976 | |||
| 8beb836ccd | |||
| b7c0e39c1a | |||
| 777acae2e5 | |||
| e77389c1ce | |||
| eb24e2e1f1 | |||
| 114244f8bb | |||
| 54769d9136 | |||
| 2f2deb5333 | |||
| 37f106d4af | |||
| 36ee7cdbfc | |||
| 2b564498ee | |||
| 0ddba16fa1 | |||
| 550086eb67 | |||
| 055ce673cd | |||
| 5308595658 | |||
| e726e29f39 | |||
| 33b12fa6b5 | |||
| 829cd05cba | |||
| 4834e12a3a | |||
| bcd4ad130c | |||
| 998d9e010e | |||
| 5480e8e1d5 | |||
| 98fdcabc00 | |||
| 236397816d | |||
| 755c55de3e | |||
| 526da71992 | |||
| 86ef262799 | |||
| 282904d4be | |||
| a1be24307a | |||
| 4b5623691b | |||
| 7bdf1e9b92 | |||
| af1db8a606 | |||
| a99bb3c4c9 | |||
| fd155c15bd | |||
| aaa43631aa | |||
| d2557bc943 | |||
| 33a3506981 | |||
| 03a54353be | |||
| c6328923e6 | |||
| 1ecb820bb0 | |||
| 0be2319288 | |||
| 073a025b83 | |||
| e83836d487 | |||
| 065c61e05c | |||
| 139a6bd903 | |||
| 3fa0ee59d4 | |||
| bd3d26422d | |||
| 370ef9fc69 | |||
| a087fb37a3 | |||
| 68c8fe0fa9 | |||
| 4309749979 | |||
| 1a677804a4 | |||
| a427e2a75c | |||
| 3c735b0ac1 | |||
| 4f446c3909 | |||
| 999ed1b5b3 | |||
| 8fa19f4a0f | |||
| 71a01ec234 | |||
| 32f033a9da | |||
| dade385147 | |||
| 6cf2e54f9a | |||
| fb673b0304 | |||
| 1a425af3f2 | |||
| 9bafed2c26 | |||
| bb2d0b0f62 | |||
| 5e4f10a80c | |||
| 9e12fc4d7d | |||
| 1caf2b7f83 | |||
| e54f71718f | |||
| 6f17e3e659 | |||
| 7f5584e4f5 | |||
| 17e2cd755d | |||
| b3513dc8f8 | |||
| 1b82dffcb4 | |||
| 5500f0d794 | |||
| c8082535de | |||
| a6970d4de8 | |||
| 7dedcb82b2 | |||
| 7195365188 | |||
| 910d0ec9c1 | |||
| bc99c1f3ce | |||
| 1d58a64ee1 | |||
| 1f77cc6d1a | |||
| 02d4dcb128 | |||
| 2b54f442d1 | |||
| 5e3ff7fc27 | |||
| ffe3f966fe | |||
| c60c19a28e | |||
| 4ea785b604 | |||
| 2d4e9d0d3f | |||
| 971d572fbf | |||
| 244e1b84f7 | |||
| e5cdc99a34 | |||
| 9a5768219f | |||
| cee8f57318 | |||
| 1a40e0a83a | |||
| d0072d930f | |||
| 385062c4d7 | |||
| 9245638b25 | |||
| 1865542192 | |||
| 2563abda11 | |||
| 59b80d8fbd | |||
| 68bb8182e4 | |||
| c979ff6696 | |||
| 25681e888c | |||
| 5cfd082b00 | |||
| 0cbced43bd | |||
| b3e8d7e07e | |||
| f4a7395e3a | |||
| 14b42abfa4 | |||
| e8022e985e | |||
| f6c8687dc8 | |||
| 472d8faace | |||
| fc5f3c2fcc | |||
| fb756208d8 | |||
| 6a98e93845 | |||
| 79e155acfb | |||
| 59ae6e3dc0 | |||
| e628ed3ef4 | |||
| 11d40e9daa | |||
| a07f0631b7 | |||
| 790d1dd8f7 | |||
| c92e510a4d | |||
| c48a6c0601 | |||
| 383f3f9834 | |||
| 3c1e9ba6e9 | |||
| 26893b9877 | |||
| 2b734b8e69 | |||
| c5f6f87a6c | |||
| 66cdb62a3d | |||
| f53e33723b | |||
| 06bc6e7568 | |||
| 5e3f42ec5a | |||
| 08b3dfa3b5 | |||
| 6cf9563441 | |||
| fb65c7f4ba | |||
| c4452909e7 | |||
| 2d3669b03b | |||
| 848e6e5897 | |||
| c723b76138 | |||
| 57f6b0af09 | |||
| 1c4082af45 | |||
| eece5d318e | |||
| bb6ade2165 | |||
| 586b010811 | |||
| 84ab0fde51 | |||
| ec18df2c2a | |||
| f50503e7c1 | |||
| c619e5c381 | |||
| e7c4a74ed6 | |||
| f8ea019f02 | |||
| 6db8dd620d | |||
| bdc1fa4c03 | |||
| 4e66a2d436 | |||
| 0fa948448e | |||
| 76c675cd09 | |||
| 85a4a594c5 | |||
| f70746c50f | |||
| 712490b671 | |||
| b580e68469 | |||
| bd2cf18fbc | |||
| 092f4217b0 | |||
| abd2ac7168 | |||
| eeea70640e | |||
| 6047838f53 | |||
| cb51799246 | |||
| 44d99277fe | |||
| 5b8e643541 | |||
| ae85c209ab | |||
| 2306caa62f | |||
| 17c11ae23f | |||
| 5b51096e37 | |||
| ac79d6bcee | |||
| b6e056f832 | |||
| d99a22d68d | |||
| 2602c155d0 | |||
| 80f562643f | |||
| 578cb4e268 | |||
| c53c6a94d7 | |||
| 907cf19f05 | |||
| 88682e1c3b | |||
| 20a4edf899 | |||
| 3222b11346 | |||
| fc9d6a6d47 | |||
| c9917e4079 | |||
| b233ab87bb | |||
| 73c3a709de | |||
| 6ce7b30b72 | |||
| 980d55a2f3 | |||
| 988be62804 | |||
| 23efd0850d | |||
| 17e0f1d9ab | |||
| 3c85bd55d3 | |||
| 2298d72ab9 | |||
| 408407b33d | |||
| ab426384e1 | |||
| 5bc68c0c6d | |||
| ebf20d5b2c | |||
| 93d9c40323 | |||
| c6ea976d7f | |||
| 5f24915300 | |||
| fbe174fb64 | |||
| 977d5331c0 | |||
| d40d7e18f5 | |||
| 11be68ad49 | |||
| b0d0782a72 | |||
| dbb6d8ac71 | |||
| a30845f9ce | |||
| 379f290b8b | |||
| 6c413bba48 | |||
| e17a39d446 | |||
| fcadf6ec4a | |||
| 231fde219c | |||
| 2774bd238b | |||
| fed67192bc | |||
| 16db970558 | |||
| c9a79bf32e | |||
| d74ed508f9 | |||
| eafba9c7ef | |||
| 610923af89 | |||
| 23dfeb13df | |||
| f4abd7d027 | |||
| b716e71784 | |||
| 094598196a | |||
| db1d1c49a0 | |||
| ff4125c11e | |||
| a0d51803ed | |||
| 3aabd63975 | |||
| 394e37f9ea | |||
| 369b88d6f8 | |||
| ec8b3ae515 | |||
| c94382b46c | |||
| 2a6a67c6cc | |||
| 37f0a9ad7b | |||
| 28540ad50a | |||
| 29d92d3e81 | |||
| 0477f354c9 | |||
| c7a0c1402c | |||
| 2af5643243 | |||
| 5e9885946f | |||
| e89879d8a6 | |||
| 2f219f83db | |||
| 63e9f794c7 | |||
| 7c0b910d7a | |||
| c77ecad9a5 | |||
| db2897cf1e | |||
| 5c5ce0dfe3 | |||
| 6de213483c | |||
| f5846b89ea | |||
| c5e7bedb37 | |||
| 2b46c560c7 | |||
| c6ad0665b5 | |||
| 8ab84dee16 | |||
| 62b2c07be2 | |||
| 2fb29ae8fd | |||
| b57e858ad1 | |||
| 054aac17aa | |||
| 68b65dd357 | |||
| f2881126cd | |||
| 11968a5888 | |||
| ad279dc566 | |||
| 2814932845 | |||
| a2430dbc53 | |||
| e51d2dd36a | |||
| 604af1ac8c | |||
| 68c6393eb2 | |||
| 4cbf9c7f47 | |||
| 8bb3b75b1d | |||
| a76f0c7cb4 | |||
| 01e31afcbd | |||
| 3b2f2f922e | |||
| 64b83b3245 | |||
| 563e6b3cdd | |||
| 6518bff2ac | |||
| 0e26247b53 | |||
| e69f7dbc5f | |||
| 4d0f6df89a | |||
| 6b184363a1 | |||
| b519069634 | |||
| 1bd44a7427 | |||
| 568ff5a3f5 | |||
| a6bf40d4e2 | |||
| b3bb99d76a | |||
| 243bab7036 | |||
| 88b39f4b67 | |||
| 5e8061f846 | |||
| 870e96a1df | |||
| 59070c2af6 | |||
| cec8936728 | |||
| 14071b0d31 | |||
| 57173e4385 | |||
| 997caad985 | |||
| 2b752c0c02 | |||
| 2cccb8b450 | |||
| 0c540ac8de | |||
| 6033b7b886 | |||
| b67f8d1389 | |||
| ae645ad9f0 | |||
| 5b72509dac | |||
| 3ce42a096b | |||
| 8331c2f267 | |||
| b3c9570b0f | |||
| 9d5c877df9 | |||
| a8e2727473 | |||
| 4b9c6e6bd2 | |||
| d29ac088c0 | |||
| 3a316de9ef | |||
| 40cb37e824 | |||
| f165b55a1d | |||
| 84b91d4575 | |||
| f5832423f4 | |||
| 73dd07aadf | |||
| 0f39a45734 | |||
| f41060c39a | |||
| bbb8e12bac | |||
| d0e1471c91 | |||
| 322ef1fd63 | |||
| 47cb97bc60 | |||
| 8d35bea830 | |||
| d8bcc4e3f1 | |||
| 434ac86090 | |||
| 1061026f96 | |||
| e638c49160 | |||
| 5d84db9fb7 | |||
| 874bdea634 | |||
| 68497d3a1f | |||
| b9e198c172 | |||
| 40d0a82342 | |||
| d49c0a1bcb | |||
| 91fb7b0a7c | |||
| 9b12c22823 | |||
| 3957006fae | |||
| 874029dff0 | |||
| 6aff3ed407 | |||
| c0ae78ae82 | |||
| 8b22f01ecd | |||
| 2ed694b041 | |||
| a0ef6ab811 | |||
| d098b39024 | |||
| 3cf23f8a5c | |||
| fc59bc2992 | |||
| da65f43983 | |||
| 72e77d237a | |||
| ecc3e18e85 | |||
| dea70af889 | |||
| 30362091e5 | |||
| ada4b6ef16 | |||
| 59e6066579 | |||
| 0aa3362671 | |||
| 5873db7331 | |||
| b3fe05ec81 | |||
| 92fbf58b13 | |||
| a4b2cc84c7 | |||
| 89c3f6fa0e | |||
| 1395da694e | |||
| 264b20535e | |||
| caba350b33 | |||
| d9fe194111 | |||
| 258adda67c | |||
| 40dc13b2e2 | |||
| 4cda54ca1c | |||
| 8116c5b3f7 | |||
| 35d584c67b | |||
| 9504cbcc4f | |||
| 1dcc5127d0 | |||
| 6790699279 | |||
| 85e3d7083c | |||
| 262ace1773 | |||
| 7cd101d8cb | |||
| ce2058aea9 | |||
| e9b0acaa8e | |||
| bd2da08c4e | |||
| 0a88d419c6 | |||
| 80c190db36 | |||
| 550cf00ee7 | |||
| fbca7951fc | |||
| 1e1358fcef | |||
| fd1b3329f5 | |||
| 9c9d8468a5 | |||
| 55ca03f100 | |||
| 83708725b2 | |||
| 6f59d62e5c | |||
| 1c348f0cdb | |||
| 634596257d | |||
| 5e4973a1dc | |||
| 19f023e0ee | |||
| 090c15fe19 | |||
| e8b307dc4f | |||
| 056479d450 | |||
| e5ebe2f888 | |||
| e8e1b431ad | |||
| 847d40e567 | |||
| cf6c555e6a | |||
| b508aa9ebc | |||
| 5e7634506e | |||
| ba39b64ced | |||
| 33ad65a105 | |||
| fcebe89f6c | |||
| 2d5eb920b8 | |||
| 26de2c86ed | |||
| cba1e95d0a | |||
| 78a5a88638 | |||
| b7b9c67259 | |||
| 54bff81470 | |||
| fe21972f4a | |||
| 58e3c72446 | |||
| 6dd5c6c317 | |||
| 4e0af3eafe | |||
| 1d0791142c | |||
| 2560ba2980 | |||
| 19be3dd852 | |||
| 40f2a6978b | |||
| 1fd8c43d94 | |||
| 63cc3fd890 | |||
| c556ca40b1 | |||
| 3e32f47903 | |||
| 8f2824186a | |||
| b0dbb20e22 | |||
| 0519c4c6b1 | |||
| 28184b4a29 | |||
| 76175abea2 | |||
| d28f829b1c | |||
| 184c3dce15 | |||
| a08a3078da | |||
| c2100d7622 | |||
| a91fa59174 | |||
| 574a6b68ae | |||
| 2f4c1dfcc4 | |||
| 1b62a21dbd | |||
| a78825eff9 | |||
| 0bad7b213e | |||
| e4bb37b1a8 | |||
| 54c443ac68 | |||
| 3af9af96ea | |||
| f75d188131 | |||
| fc3a00054f | |||
| 84e41c2ade | |||
| 4630733b55 | |||
| eb690e14e4 | |||
| 009430e829 | |||
| 073fb73ff3 | |||
| e789747834 | |||
| 833002f846 | |||
| 3838fab788 | |||
| 907e9fc476 | |||
| b829a39cd2 | |||
| daa7af0605 | |||
| 47a1adc864 | |||
| 98e448acdd | |||
| 72bd51f26e | |||
| 29db856322 | |||
| cd5cda916f | |||
| 33a1139772 | |||
| c5b62903f3 | |||
| 387fd16b40 | |||
| c91b67d370 | |||
| b809d1c263 | |||
| 7bb6675abf | |||
| b91bea94f4 | |||
| 96e21700bd | |||
| 7e8f25bce3 | |||
| 9e02049b05 | |||
| affdfccd60 | |||
| 402f8c27a9 | |||
| ba4dc6c60a | |||
| 6b8dd42547 | |||
| 1511a27f4c | |||
| 7ae6c147fa | |||
| f51630eb07 | |||
| e3586411e0 | |||
| a0639a32c7 | |||
| 759c6e77a7 | |||
| 04ad3d7c3c | |||
| 49badd9a2f | |||
| 8b00083bca | |||
| 3d98e324b5 | |||
| 768c66313f | |||
| 3561fd1c05 | |||
| a6c055b6d1 | |||
| 0d24c18fed | |||
| dd8b2a79fb | |||
| f0095611bc | |||
| a3567f0918 | |||
| f0d3d0d74e | |||
| 3e32bc0d5d | |||
| 632e4aa120 | |||
| 2391ce198d | |||
| e5e2bbd482 | |||
| 0b6632123b | |||
| b1801fc953 | |||
| ca1a1c4f28 | |||
| 3363cc4f1d | |||
| f84684982f | |||
| 3bed5969bf | |||
| ebc162e3d8 | |||
| f30136dba3 | |||
| 9b1926f902 | |||
| d29524ba3f | |||
| 7258fe4e5c | |||
| f8ea1702f8 | |||
| 7582c28c1a | |||
| 5042eb87e7 | |||
| e8e80732ef | |||
| b8744a79ae | |||
| 414b153d28 | |||
| 8e160dda8e | |||
| 9b54c9b807 | |||
| 1bb608cdb6 | |||
| 1239485b30 | |||
| 0d23d047fc | |||
| d837ae64ac | |||
| d72a70396a | |||
| 938772b86a | |||
| 3e88593a81 | |||
| 60c01d7869 | |||
| 1cbcc61bd6 | |||
| 7f5a2974ce | |||
| 3de3ea38b9 | |||
| 3659e86d57 | |||
| c335a6b3de | |||
| 267b831bc4 | |||
| 7ee93cb910 | |||
| ae95a49618 | |||
| 8f98504183 | |||
| 1b77ee0ef4 | |||
| a6de395cde | |||
| fcd6dd34b2 | |||
| a6ebfe4215 | |||
| 6a9158aa62 | |||
| b0b0291bc7 | |||
| 85f1da1f10 | |||
| c47445ca98 | |||
| 4e25867548 | |||
| ad71bb30ac | |||
| 362bf1895d | |||
| 7d00c0bd5a | |||
| 5e5994f166 | |||
| 7247762b60 | |||
| 21e0c79f7d | |||
| 78b08bfef2 | |||
| ae7e90dc2f | |||
| bf873bde42 | |||
| 017f81e430 | |||
| 0028bfbfc7 | |||
| 60c9c403bd | |||
| ec5fff2046 | |||
| a7199a3d0d | |||
| 5cf2ebea4f | |||
| 580e95605e | |||
| 8c3d1df3cf | |||
| 7c66f91429 | |||
| 73e294b1bd | |||
| e5ec479923 | |||
| 75107f99b2 | |||
| fb8efe368a | |||
| 1faf477537 | |||
| e3d108454c | |||
| 806b40727d | |||
| fa702efe8f | |||
| 344e3e18ab | |||
| aea9eaa307 | |||
| dffe0b39b6 | |||
| 267d660527 | |||
| 4c3046f917 | |||
| 71444b638b | |||
| 962ec7bb53 | |||
| 52fad6aec2 | |||
| 5b830f0b6a | |||
| 3d24c8768f | |||
| 977b8625f8 | |||
| df7dc04a1d | |||
| 269d3cb086 | |||
| 4d310cd461 | |||
| 88c5c39fcb | |||
| 79ca68300c | |||
| baca20b225 | |||
| 0e3cb1977f | |||
| 8b1fa72877 | |||
| e8610a35b4 | |||
| 77e6442f73 | |||
| eeddfd4919 | |||
| fa16da86b3 | |||
| 372a628cab | |||
| 2f4d8c3530 | |||
| 482eab0e2a | |||
| 03c63d9b12 | |||
| 07e87915ba | |||
| 91f2bf99c0 | |||
| 535d59db4d | |||
| 9739c3355a | |||
| 827db37eef | |||
| 0c6e47a5bc | |||
| 864ea749e5 | |||
| 5d92ec3b7b | |||
| 733a3ed102 | |||
| b14be026b7 | |||
| b4afe97289 | |||
| 43a7a607b2 | |||
| a8bf66d8af | |||
| 3616a07dbb | |||
| 6609dfd410 | |||
| e3913bd397 | |||
| 8c01ed1469 | |||
| 7aa0dcc89f | |||
| 5285b22a76 | |||
| 0fa49bc2cd | |||
| 01d8730850 | |||
| 52149ce74a | |||
| bffc20612d | |||
| 2c0eb19a27 | |||
| 748c4737f6 | |||
| 769d5113f7 | |||
| 157be6da05 | |||
| 1dc4b8bb63 | |||
| f261599435 | |||
| 2862b49057 | |||
| a8d0d8f33d | |||
| f55a2079bf | |||
| c0f706a2a2 | |||
| b034f67a0f | |||
| 977b9eb686 | |||
| 5e11bf735e | |||
| a8c73f7a4d | |||
| 86105611fc | |||
| 0364af7337 | |||
| c618ce4625 | |||
| 2b9c834476 | |||
| d366ec9c48 | |||
| ca3981fba8 | |||
| bb490faefe | |||
| d8f673ed51 | |||
| 6ce7170cf4 | |||
| 1d71e7243f | |||
| cf08901d02 | |||
| 230a9311a0 | |||
| 576f7142c1 | |||
| 20b4285849 | |||
| d67bdbf088 | |||
| 3a389793ff | |||
| f5ff5dc3e0 | |||
| 00bf5bdf69 | |||
| 9541aa7dbf | |||
| e61c6b89c8 | |||
| a5b3869e9f | |||
| fbdce27db2 | |||
| 148876f597 | |||
| 0cb533beca | |||
| 5811ebd6f3 | |||
| 8fa87f8ba5 | |||
| 21ba4f71f6 | |||
| 097e7df7c9 | |||
| 83c6615d6e | |||
| f6fafeaafb | |||
| 420a88c776 | |||
| 5fcf9481b3 | |||
| 48c3dcc08a | |||
| 62333b3e2c | |||
| df758b31b7 | |||
| 9f08bfaa6f | |||
| 198d2c780d | |||
| ab1c0dabae | |||
| 0234f11914 | |||
| 79fcc9f343 | |||
| 031f722540 | |||
| 531ccf1819 | |||
| 0d2ac42dc4 | |||
| 9ec6ea3bdf | |||
| 1ce580bba3 | |||
| 8ad2a94a90 | |||
| de3f75bc83 | |||
| 008d85ed32 | |||
| 5e30aff418 | |||
| 5de0d39553 | |||
| d95d44dc94 | |||
| 6061deac37 | |||
| ba34a766e7 | |||
| 1c81a17298 | |||
| 2097b31d4f | |||
| 2155dd0552 | |||
| 8733654094 | |||
| abd15748ce | |||
| 9a796f1383 | |||
| 88f2f62945 | |||
| 30adefed07 | |||
| 20a1828fa5 | |||
| 809674ca2b | |||
| 0ca3475878 | |||
| 32b741e205 | |||
| c917c4a468 | |||
| 6c584d2b4c | |||
| 759d415d40 | |||
| 45d86fa270 | |||
| 2c5cad71ee | |||
| 2b5925b893 | |||
| f012ada2c4 | |||
| af1b26ae95 | |||
| f72f5b43e1 | |||
| d55618921b | |||
| c7e1e07262 | |||
| 24a1bec23d | |||
| 89ad104423 | |||
| c2f3324302 | |||
| 04a969b997 | |||
| 630dfa9499 | |||
| 95668950c2 | |||
| 3012501e4b | |||
| 0e81dfb004 | |||
| 35b7f358b6 | |||
| e3e48944e0 | |||
| 94bbba72f5 | |||
| b34716f7e9 | |||
| c429ca67b9 | |||
| bce2ba0785 | |||
| 2613690064 | |||
| 7283076bc8 | |||
| f43d05b54e | |||
| c6b500bc09 | |||
| c3972015c7 | |||
| 3c18c57857 | |||
| f562a06707 | |||
| 7f50dd205f | |||
| c4fe15400c | |||
| 6e3e8f7310 | |||
| 5ae2c26130 | |||
| d8d35f4022 | |||
| fadb4d9219 | |||
| b63149b36a | |||
| 79f92abcfa | |||
| 0137fb468b | |||
| 70ef8760cc | |||
| c74d2d831b | |||
| 0415f821eb | |||
| 779fe35255 | |||
| aec7ef6f9c | |||
| 329f09ce0a | |||
| 68c23af5ae | |||
| a54f30c02f | |||
| fde00b1c62 | |||
| 11382d2cd7 | |||
| ef31131a5d | |||
| 8dd425f8ff | |||
| 76feabe32b | |||
| 7fe3e2f90a | |||
| c0b2151929 | |||
| 5e3b1bf6b0 | |||
| fdf4523c2a | |||
| c6cf76f345 | |||
| f16e544691 | |||
| df101217fc | |||
| 5d90dc16cc | |||
| bbfc1a1cd6 | |||
| b7208c12ac | |||
| eaa2fdec44 | |||
| 7099adfe79 | |||
| cb17a2bcb0 | |||
| 172044a1cb | |||
| 69204d4fb3 | |||
| 4b203b6b63 | |||
| 67876bab4c | |||
| 6ce691e40f | |||
| 9cd44b09f9 | |||
| 73a2704126 | |||
| a50dd785b8 | |||
| bafbe5cbec | |||
| 9cdcbf6bf8 | |||
| 9596087959 | |||
| 6570402b95 | |||
| 4153845346 | |||
| 548713ed98 | |||
| 1bf1ce7070 | |||
| 21dc0fbf2f | |||
| 7b29de9698 | |||
| 7d468ee148 | |||
| 260e7b529f | |||
| b027089fc1 | |||
| 8f318528f8 | |||
| 9696e70817 | |||
| 85580acbff | |||
| c45a5f7e84 | |||
| df74b0b038 | |||
| 749e5b8458 | |||
| 1255566324 | |||
| a25da4ebf4 | |||
| 72835f3c5f | |||
| c4f52f6256 | |||
| 3eb9505633 | |||
| 543a0be1e4 | |||
| 5ef21c5075 | |||
| 5c412cc0bd | |||
| 346deb4b0d | |||
| 6f8f30b018 | |||
| b808757012 | |||
| 5e491bb89a | |||
| fc3cb0dd45 | |||
| 0846cb04af | |||
| 9708394846 | |||
| 92e4396602 | |||
| a5870b73a9 | |||
| 77ff6b9088 | |||
| 425e74f297 | |||
| ffa184464e | |||
| eca68e9e7f | |||
| dd1bc8ec9a | |||
| 24dc1d6991 | |||
| 9f510b7eee | |||
| 730ca9b60b | |||
| 1afaf903f9 | |||
| d53cd41aa6 | |||
| 3e5ea745d2 | |||
| c7052f7dc7 | |||
| 56b01df85b | |||
| aa18eeb7d6 | |||
| c18264c615 | |||
| 11746290a9 | |||
| 16c062c069 | |||
| 64396de0dc | |||
| 8ff78c5d60 | |||
| 349297e495 | |||
| dc3ffb3b30 | |||
| 5c2dfb138a | |||
| 0be679de42 | |||
| 8bd68e0f10 | |||
| df70e16b4d | |||
| b246545da5 | |||
| 3280cb648f | |||
| 8798bf42e6 | |||
| 5a23927e56 | |||
| beafd597dd | |||
| fbc43b0d58 | |||
| facfcf679d | |||
| 68b230a78f | |||
| 3d1fcc6f83 | |||
| a0578efeb9 | |||
| 727ad5755e | |||
| 4f17352858 | |||
| 0eb72122ce | |||
| 66e2b3bb70 | |||
| 5f12d858eb | |||
| e258d6ca8d | |||
| 2d25dedbcc | |||
| bdf6fcb222 | |||
| 7e1cea1ef6 | |||
| d98c803b54 | |||
| 95238466b5 | |||
| 71652043a0 | |||
| c9cbaf254b | |||
| f9cc5cbd33 | |||
| bcb9405793 | |||
| 30cb6f196f | |||
| 856ef01632 | |||
| 6f95554655 | |||
| a72f915646 | |||
| 94605417f6 | |||
| 1e017df128 | |||
| ec27bb5131 | |||
| 9637fc098a | |||
| 874020ced7 | |||
| a7beedcfb6 | |||
| 4351c4dd6f | |||
| 1ddf7fb96c | |||
| ec5cfe4ee9 | |||
| 4fed2ea7bf | |||
| ae14cf4740 | |||
| 8aa68b3dc1 | |||
| e810ee7750 | |||
| 9a08194597 | |||
| c77277b60c | |||
| 21a324558f | |||
| 7a31751564 | |||
| b11bacc2e2 | |||
| 8c02e7ba67 | |||
| 275eb8d434 | |||
| 736d0df38d | |||
| 89d5d41015 | |||
| 0e1444c84b | |||
| 104f8b093d | |||
| 1e638c376b | |||
| a2e1a6ca8f | |||
| 337331ff1b | |||
| 1dfde7cd80 | |||
| 7df2bfe7bc | |||
| 4ec90a4b99 | |||
| d4e8f9039c | |||
| 4c6c00f16d | |||
| fd30b25596 | |||
| 032b7cab5d | |||
| 47dfb4b8cd | |||
| 51e782b671 | |||
| cf195262bf | |||
| cf72052e46 | |||
| 6f50c39b2a | |||
| b6cd826dd7 | |||
| f1194b1fbe | |||
| c0ca85fb3a | |||
| 022df1b143 | |||
| fdf987f081 | |||
| f1e874cd18 | |||
| c3bede58aa | |||
| 38915eb7fc | |||
| 470bd23b3b | |||
| d007eefe2e | |||
| 362f442a98 | |||
| 8c2645c5dd | |||
| 1ba0e4809e | |||
| 341371b613 | |||
| d856285271 | |||
| e4ffc93463 | |||
| 3149958319 | |||
| ac659e8df1 | |||
| 81d54c7558 | |||
| e4de333d83 | |||
| e72096328a | |||
| 88a082a533 | |||
| 4fbf4f1069 | |||
| c360dd11ed | |||
| 95d582ccee | |||
| e0c9b990e7 | |||
| 074cfb7c58 | |||
| 8b649cec8d | |||
| ea6974fc89 | |||
| fb29da4e40 | |||
| 461acbcc81 | |||
| 5bab8647b6 | |||
| f2d1222de7 | |||
| 585ea14a23 | |||
| 5a0997ded5 | |||
| 0174c5674f | |||
| 9de8653936 | |||
| 27d28b8247 | |||
| 110f43a246 | |||
| 56612751f9 | |||
| 751fe7349a | |||
| fb1b554b86 | |||
| 36d7d33afc | |||
| a94f3c720e | |||
| 6c1087e429 | |||
| 2cdb010cff | |||
| 60052f59a0 | |||
| 29d44f809e | |||
| 83e4aa2755 | |||
| eeb97f5b66 | |||
| b6f26ae6a5 | |||
| f40435654a | |||
| 67d471ea3d | |||
| 4946c5e687 | |||
| af4f05c29e | |||
| 145c76095a | |||
| 90045b6faa | |||
| aef27d811a | |||
| e2e5f80298 | |||
| ca0ed50172 | |||
| d44d63c1d6 | |||
| 8403042297 | |||
| 1568bb014d | |||
| 7a069c4018 | |||
| c856eb931f | |||
| b290ce795f | |||
| 6f3d279165 | |||
| 124ab30f98 | |||
| fee90bab66 | |||
| e26ade0e62 | |||
| c43ccb860b | |||
| 81de2b3afc | |||
| 9a53fa3876 | |||
| f2b7e8b038 | |||
| 1863f1311b | |||
| e5086f22d6 | |||
| da90a3ca78 | |||
| d397c5a251 | |||
| d26b4434b8 | |||
| 7188f17f9a | |||
| b0365f8b0e | |||
| a1ddeea00e | |||
| 1414c24caf | |||
| 0748c864cd | |||
| f80626a0fa | |||
| 0fffd64a7f | |||
| 8da48211d9 | |||
| 0dd8ffa3a0 | |||
| 0362e61f36 | |||
| d92a77f695 | |||
| e6dd573e8a | |||
| b6330c3a4f | |||
| 10bc714f5c | |||
| 8f57723b88 | |||
| 3d71bee85e | |||
| 30e00d5fa7 | |||
| bc99a9d792 | |||
| 61df41d21f | |||
| 0b4ef8dcbb | |||
| 9d7d48b9b5 | |||
| fb37150dfd | |||
| bd08ed898d | |||
| d3bc525713 | |||
| ae27c553ac | |||
| 27030ae1e9 | |||
| fd083e1e66 | |||
| 5e4149ae76 | |||
| 859d462629 | |||
| 8bfa81df42 | |||
| 6782d53e28 | |||
| 4796721d5c | |||
| 55bbc71a17 | |||
| 5372575b24 | |||
| d995019c6e | |||
| aa70da5659 | |||
| 020b293068 | |||
| 678ff23bcb | |||
| f93d50dcd0 | |||
| aa3201ebb0 | |||
| 4ad153c425 | |||
| 082683bf0e | |||
| 7f590af0b5 | |||
| ecc1c86600 | |||
| fece506cdd | |||
| f11a58e2cb | |||
| 0238ecebed | |||
| 0d6ffa3935 | |||
| 143632e635 | |||
| 9ec33a97bb | |||
| 7e2c236582 | |||
| 6ebfd175bc | |||
| defaa918a6 | |||
| c4e70be0a5 | |||
| 57d425fae6 | |||
| 6024163af8 | |||
| 44b35cdb3d | |||
| 36ff0ad019 | |||
| 9218e518f1 | |||
| c80bde1f60 | |||
| 4b7157b987 | |||
| a90f592224 | |||
| 59f228dab7 | |||
| bae3f5ceb7 | |||
| a5c5da5b8a | |||
| 7ecf313132 | |||
| 313cfacfa1 | |||
| fb991503a9 | |||
| c31ce641a1 | |||
| 5b1a5b7dd0 | |||
| f25324fb1c | |||
| 2a7f35a633 | |||
| eb2d5484b8 | |||
| 40cbd5ec9d | |||
| 10680ace17 | |||
| 26e28ed687 | |||
| 8bf92d84db | |||
| 9e2bb5b37b | |||
| a48a88c312 | |||
| 76b2fc2a6c | |||
| 4438d716b9 | |||
| 8fcf55d761 | |||
| 3091a76702 | |||
| 117e2370d7 | |||
| a35d70e995 | |||
| ec68000105 | |||
| c707d3db00 | |||
| d3572836bd | |||
| a5dac751b0 | |||
| 7a59579dcd | |||
| a60e4efe6d | |||
| 4c0ebeab58 | |||
| 0143ac2a86 | |||
| f24b02cae4 | |||
| 497a2bd057 | |||
| 995f796a5d | |||
| bf462e2840 | |||
| fb75134179 | |||
| 68d67b7fc2 | |||
| bbf412f3ad | |||
| 52b575296c | |||
| c1652f4898 | |||
| d40cc795f7 | |||
| 4035d933ad | |||
| d255348762 | |||
| 978db89deb | |||
| b867afe772 | |||
| e34fd89bb6 | |||
| 51883b8f11 | |||
| ff5f95227a | |||
| dc929236a4 | |||
| b09e20747a | |||
| e8fc857dbc | |||
| b42a13cc3b | |||
| 37ed9800c5 | |||
| 99f4968888 | |||
| 34428a811c | |||
| da7104b00d | |||
| b373601e13 | |||
| fcc90e884b | |||
| 7a90096077 | |||
| e7886a55fe | |||
| e33b786f65 | |||
| b0b50f4ef9 | |||
| 37c8e6dc07 | |||
| 675441fe93 | |||
| 2495d97862 | |||
| 70c998dd55 | |||
| ec60e46611 | |||
| bb2dc618f7 | |||
| d9addf84ef | |||
| d89163110e | |||
| 7c851faba6 | |||
| b70c219a05 | |||
| 93b722af3c | |||
| ee0e036aa5 | |||
| 24ef004adc | |||
| dcab4eb70b | |||
| 8f252992e4 | |||
| a3a3e32e21 | |||
| f5f8867326 | |||
| 5494a9dd3b | |||
| 89f7857c84 | |||
| 3140c6526a | |||
| 96d828d22b | |||
| a8a50384b8 | |||
| 787af6448d | |||
| 4633135322 | |||
| 050d522735 | |||
| 809b202e70 | |||
| f1555dbb5d | |||
| d1d0266a10 | |||
| ee37ed0697 | |||
| e3972dee2c | |||
| 7a727e7eda | |||
| d24c822f68 | |||
| 8d804013f3 | |||
| d73a115436 | |||
| 0b4ff731e8 | |||
| a58ec3f192 | |||
| 868c20b161 | |||
| c2cd050419 | |||
| 7d5c107fb8 | |||
| d855a6ea0f | |||
| 464f84d8cd | |||
| fe0ee6402a | |||
| 068939f790 | |||
| 35f48d1c8e | |||
| 52adde2501 | |||
| 0ddc4eceaf | |||
| b0ab8c750d | |||
| b17dd8351f | |||
| 0ceb8d159a | |||
| 402b943ddb | |||
| be55451c90 | |||
| c51c1a2ae6 | |||
| 845c796b96 | |||
| b0918ef293 | |||
| 102572b088 | |||
| 63076e77f5 | |||
| 8e48ee5f66 | |||
| 1a55f550c0 | |||
| ae8fc64394 | |||
| 5e8e56caf9 | |||
| c075c161c2 | |||
| 237a553d15 | |||
| ca8674e0de | |||
| 0511a1172f | |||
| e07b304914 | |||
| 17364e72ec | |||
| 22b213ae26 | |||
| 7d5936a9e9 | |||
| ab8f466f53 | |||
| 201177e7f0 | |||
| ec5f9a2892 | |||
| ee5b8748b5 | |||
| 8d04f8b8b5 | |||
| 033babfbfc | |||
| 15b77861ea | |||
| c4721850ce | |||
| b325aad5c9 | |||
| 92e616f18e | |||
| f7fee29c76 | |||
| eccea7411f | |||
| 01f93e0970 | |||
| 2d82a7bc2e | |||
| ca91fba071 | |||
| 9f2fce4d87 | |||
| e1942267c5 | |||
| 12212409c7 | |||
| e5565c6bdb | |||
| f00558d840 | |||
| da0dc5ed11 | |||
| b417492fad | |||
| d3ee532624 | |||
| e8be38ce5a | |||
| 38c9a05a0c | |||
| 110bd332f4 | |||
| 8a0f73bf81 | |||
| 337c9cbea3 | |||
| cfd61096d9 | |||
| 2894e253a2 | |||
| e52985e082 | |||
| 7d2bc12bb7 | |||
| a5f397b26d | |||
| 5b93d5210e | |||
| e943a6e09c | |||
| 8f527a6212 | |||
| f2f8ad6b65 | |||
| c870930bc0 | |||
| b26b1caa86 | |||
| 6613ee6b0d | |||
| 9550bca099 | |||
| 92a75aaa08 | |||
| 906bf88450 | |||
| d7157843f4 | |||
| d317c1ff08 | |||
| ef889963d9 | |||
| a2d7b221ee | |||
| aff32afefa | |||
| 0943e0c60f | |||
| 18f75ec61c | |||
| d821082843 | |||
| 366a88cc5c | |||
| 951df61aa0 | |||
| 3e79575602 | |||
| 92e24777c0 | |||
| ab8d06bb86 | |||
| 8563dd5860 | |||
| 1f6153fa82 | |||
| 23d66b9746 | |||
| 25ccd6bc6d | |||
| 14ad32bcd2 | |||
| 9ab9b9d75a | |||
| b7a3c4557f | |||
| bdb90b4b33 | |||
| 85d0935e97 | |||
| 86ad75d27b | |||
| b40473aa3b | |||
| 3bd5ffc5cd | |||
| 10aafd3738 | |||
| c055765bfe | |||
| d8f486fc0d | |||
| 06eea71a37 | |||
| 3effb9ec29 | |||
| 6603a2300b | |||
| ed029fe348 | |||
| b497bc5eb9 | |||
| 8a4a1dfadf | |||
| 8bbf14acbf | |||
| 86f2c86440 | |||
| cfb29f1339 | |||
| 63a28d8e34 | |||
| d37cbb10a5 | |||
| 055590c0c6 | |||
| 6aaac45468 | |||
| 34adaae5af | |||
| 9c6f004f7f | |||
| ff685e33d5 | |||
| 2999603b28 | |||
| 32d8f4b084 | |||
| 0fb0c1b71b | |||
| 2ac34dbab0 | |||
| 986fb12543 | |||
| e686eb750f | |||
| 8ac15068ee | |||
| 5e4cd6cf11 | |||
| 39d694de8c | |||
| 342f5c01e0 | |||
| f91293c6c5 | |||
| 1ce4977a70 | |||
| f4b25b59e5 | |||
| b33a47e253 | |||
| ccd4d4263d | |||
| 2ff9a36eed | |||
| d1e91cd702 | |||
| e2599071c5 | |||
| fc38b89aee | |||
| 5688286a79 | |||
| 7eb10ab7ac | |||
| 34b31865c5 | |||
| f1c5b632cc | |||
| 04ca0ac2b5 | |||
| 8b2fdf3a75 | |||
| adca75b7d8 | |||
| 504fa2a1d3 | |||
| 266a062a5d | |||
| 652a9452c2 | |||
| 503b6ea6c8 | |||
| 547501ba81 | |||
| cfffbc4a09 | |||
| b58d84fba1 | |||
| a5d3dd942e | |||
| b96062b6de | |||
| 04b71c11e1 | |||
| 651baefb1d | |||
| ff7e845615 | |||
| f0612a1407 | |||
| 83bd24adf8 | |||
| b5a8e6bbdf | |||
| 9798fcf839 | |||
| 15556b6797 | |||
| 0ca4d728d8 | |||
| b2c7804032 | |||
| e091dc0294 | |||
| 3bfb4595cf | |||
| 8955d8de23 | |||
| 1372b298bb | |||
| 9558845e6e | |||
| 5ab0930de8 | |||
| 3294f4858a | |||
| eea9a3ba59 | |||
| 753974d663 | |||
| 527cd0a6e5 | |||
| 24f70387d2 | |||
| adc2070ac1 | |||
| a8642682d0 | |||
| d66e6db480 | |||
| dc66bbc3dc | |||
| 6e7f5feea5 | |||
| f21ea6c065 | |||
| 6af56b56bc | |||
| 598d40b0b7 | |||
| 6ae714f51f | |||
| b0661bb586 | |||
| b6a165f1f8 | |||
| 8fe4a36b68 | |||
| 0d24f2d4c1 | |||
| dd0ff3eeb5 | |||
| 07868f701a | |||
| f4f0e4b60f | |||
| ae950a2ff4 | |||
| 5f6e4bdfe9 | |||
| c6d2d4ccda | |||
| 59160a5d42 | |||
| 5da6423fd6 | |||
| d36b8721ca | |||
| 539abffe0e | |||
| 9b24e66441 | |||
| cc16cb9281 | |||
| 45fe4846f2 | |||
| 3ca2779d9c | |||
| 967341b127 | |||
| 4e7f9fb805 | |||
| f3eb661aad | |||
| 1abf8e23a4 | |||
| 9f1f476f43 | |||
| 1a9d61c92a | |||
| 6ad465e3c0 | |||
| 8ef947722f | |||
| 6e6b5c95a3 | |||
| fa593a7a37 | |||
| 7fcccad0ae | |||
| e8ce94ade2 | |||
| 6055f038ee | |||
| 6a1f40eeab | |||
| ca01589e50 | |||
| cca891644d | |||
| cd19578d80 | |||
| c96f7e5a13 | |||
| d7f92b4f72 | |||
| 70a5208fcc | |||
| 8c9150db66 | |||
| 1f86dbd12f | |||
| cfa871c076 | |||
| f355661522 | |||
| be3fb0f917 | |||
| e2f4c0ffd1 | |||
| 210a53a3a5 | |||
| 5049919855 | |||
| ce187786cb | |||
| e6b35a9237 | |||
| 82e5e9cf4a | |||
| d7e1910076 | |||
| 009c28ae50 | |||
| db66023102 | |||
| 4d8dc1a0c4 | |||
| e0a5edeb04 | |||
| ffd9a01e2f | |||
| 25a8c79951 | |||
| c8674ff104 | |||
| a40b10f53c | |||
| 79fa944402 | |||
| ed3cdeec74 | |||
| 05d50d457c | |||
| 2531db84a6 | |||
| 96c1126fe5 | |||
| bb5038b8b2 | |||
| 0c65162349 | |||
| 17cc12844d | |||
| 6cfcf92a28 | |||
| 6ed9a85dca | |||
| 0371265fea | |||
| de257b34c0 | |||
| 4b6575d94a | |||
| 2c54d76085 | |||
| 70f39ed760 | |||
| 1c6652483b | |||
| ab7e0a9266 | |||
| ff323d00af | |||
| ea2a04135f | |||
| 6d88c76464 | |||
| 9b188ca87d | |||
| 1664312c80 | |||
| 38baa42ebb | |||
| 654322e896 | |||
| 3f70f532b7 | |||
| 6ba214a259 | |||
| caf73f387f | |||
| 9a81ca9fab | |||
| 0edf19a871 | |||
| 6989f6c835 | |||
| 2daa39520a | |||
| c8eca50f43 | |||
| de844f1a32 | |||
| 97951e1c1a | |||
| 2edbed8528 | |||
| 24937910c7 | |||
| 5cd441fb48 | |||
| 06b956bd75 | |||
| 41864d46c3 | |||
| f6622e0bcd | |||
| 0f30d21fa2 | |||
| 4257c8c9f5 | |||
| 331859d383 | |||
| ef03b708a8 | |||
| 716d098361 | |||
| d887057660 | |||
| 7efbfebb4d | |||
| 4c7afe5af0 | |||
| 676515cf27 | |||
| 0eb5b0fdfa | |||
| 2feba4787f | |||
| 516dc1043e | |||
| b26c1c57dc | |||
| 0945ba9e90 | |||
| 69ed6f283d | |||
| 9eef850d0c | |||
| cf1574d690 | |||
| d6913e41a0 | |||
| 3c81c295c7 | |||
| 56dfa0c755 | |||
| 43989be768 | |||
| 822380ac38 | |||
| 8c37d9ac9a | |||
| e40b8461f7 | |||
| a3f45b466a | |||
| 672ad68c64 | |||
| 4ccec13739 | |||
| 09529a1aa8 | |||
| d182fd6bb7 | |||
| 36bf123e2b | |||
| 92cfbf655f | |||
| fbef701179 | |||
| 0415b9cf4c | |||
| cb9a9e8d50 | |||
| 6021c1c6b1 | |||
| 655be2fa2e | |||
| 98491a63a7 | |||
| acd7f15c83 | |||
| 5020d4e99f | |||
| 9693c30209 | |||
| 2b6f8adc64 | |||
| 822f5927e5 | |||
| 0f6e9d7b9d | |||
| 99f3e3f09e | |||
| aa81c96a98 | |||
| 9d532b6c72 | |||
| 4c63906b8f | |||
| dd2a870227 | |||
| 88948c3cfd | |||
| b33dcfe6ff | |||
| 2c15bdae04 | |||
| 2f45633312 | |||
| fdd42fbc6d | |||
| 54a6f5d425 | |||
| 68d9662fe5 | |||
| 4f0987da01 | |||
| 625697e097 | |||
| 92b14f20d2 | |||
| dd069647d1 | |||
| 4523ae7d29 | |||
| 19e5eda773 | |||
| 070d58ac0e | |||
| 2d70f69857 | |||
| 7a3acfa6a7 | |||
| 8ac0d12d1e | |||
| 86164103f0 | |||
| b9c71ef03f | |||
| 7ffff761d5 | |||
| 7d4366473d | |||
| e63c660162 | |||
| 1762f9d68e | |||
| 76287eed2c | |||
| 5a764bbaa2 | |||
| ce9e69c9e0 | |||
| 3a74e1f154 | |||
| 6df4a36da9 | |||
| 4e38b51958 | |||
| b6c036af25 | |||
| dd789a8dcc | |||
| ca83b858c0 | |||
| 0f29952a1c | |||
| e2d7b465ae | |||
| 8985dc2f7e | |||
| cf1731792c | |||
| 4c200cdd49 | |||
| 5c8eacddde | |||
| 2668177210 | |||
| 62be08f063 | |||
| ab2a67a012 | |||
| 3ceeee7298 | |||
| 2d7576f29b | |||
| 6e25a17afb | |||
| 0715682a8b | |||
| cfff30c314 | |||
| b53318ecb7 | |||
| 039a3e258b | |||
| 18806e5524 | |||
| 0594a8d03a | |||
| 5a575d61b6 | |||
| 42c3cf2545 | |||
| bf6490739d | |||
| 9815c0a866 | |||
| f5f05a9a91 | |||
| b392656d60 | |||
| 6737d091fd | |||
| 9f924c3510 | |||
| 2ea66d2e81 | |||
| d18c238938 | |||
| 8c92e221a3 | |||
| bafe9c06d4 | |||
| cad6ec854e | |||
| 47a0398b62 | |||
| 11f5ae3c20 | |||
| 61cf853eb5 | |||
| f72884ac19 | |||
| b72b38b0a3 | |||
| 6a2465329a | |||
| 4cb80588e9 | |||
| 753f11e0e9 | |||
| c0bd2c8945 | |||
| aebbe4f254 | |||
| 68948dbaeb | |||
| a38917f920 | |||
| f52e198b17 | |||
| dec734346b | |||
| a73f10edd4 | |||
| 59a7232016 | |||
| d9e6aed9da | |||
| e4f52dd1c7 | |||
| 2c1e3416e3 | |||
| 62090ef119 | |||
| 52ef8a635f | |||
| bf26ccd0a5 | |||
| 5a55b98650 | |||
| 547333c946 | |||
| 1ed105cb79 | |||
| 2ce2928170 | |||
| 14727d75ac | |||
| ccbc0b79b8 | |||
| 5bee0004b2 | |||
| 86fd42dcb5 | |||
| 1e05e0d6f8 | |||
| 821e0ed6ce | |||
| 66ce31f6d6 | |||
| cf486aedbd | |||
| 1b0f22c4ae | |||
| 55acf21aa6 | |||
| dc8a2670ab | |||
| b666ec1f4d | |||
| 999fc07683 | |||
| 37a186696a | |||
| 107ef27f69 | |||
| 0c1c10a0e0 | |||
| 89de1f9a01 | |||
| 602e91da40 | |||
| dec4e67135 | |||
| c30670000d | |||
| 9d8e81d79c | |||
| 421a35c201 | |||
| fcfc7b6cec | |||
| 2f5da3851b | |||
| 6c2e8eba1c | |||
| c9c3937f4b | |||
| 7777cbf6da | |||
| a8a7d327ff | |||
| 571fcbe98d | |||
| 8b4b0e0d39 | |||
| c8868a393b | |||
| 49be37dcf9 | |||
| 2cd5fe2fec | |||
| b52a674c1a | |||
| 8f790d406f | |||
| 7e2a256229 | |||
| c7a0a560d8 | |||
| 1d591034ff | |||
| f5ceaffc5c | |||
| 3c246a97e8 | |||
| ca395541b6 | |||
| 30d9dec438 | |||
| ff3606478c | |||
| 72caf1886d | |||
| bdae9521cb | |||
| cb7fb6c7be | |||
| a33e4477af | |||
| d435192e22 | |||
| 9480447637 | |||
| 7dbe852606 | |||
| d7216f44f5 | |||
| fdf09d46af | |||
| 32360e7473 | |||
| 90482377b7 | |||
| 08a3aea1c7 | |||
| 6eebd1e957 | |||
| 033bd9bbdc | |||
| af634d3a7d | |||
| d42ce3935b | |||
| 7a239c81f7 | |||
| 2b96cd7059 | |||
| 36b8b2c679 | |||
| 69b3be2419 | |||
| bbe74e6987 | |||
| 98d606fca4 | |||
| 5c4472492a | |||
| a5c726eef9 | |||
| d23d5b50a3 | |||
| 37e507707d | |||
| 44d889b99a | |||
| 491092d040 | |||
| b296a1dabe | |||
| 63b8d45ef2 | |||
| 8220fad855 | |||
| 5aa146f0a6 | |||
| f8d2661426 | |||
| 25b8027bd6 | |||
| c93c8a79b7 | |||
| 29336e260c | |||
| 29942e5109 | |||
| 926fee8493 | |||
| eedaacd256 | |||
| e926aa1bf8 | |||
| 597f981fec | |||
| 777fdfbcfa | |||
| aafb587085 | |||
| 1eb2576dbe | |||
| 9a9646d012 | |||
| 8ecb05a094 | |||
| 8c8ca0584f | |||
| b02ba08abc | |||
| 75d213f6b3 | |||
| 9fdeb7a8e3 | |||
| 635b3dbccb | |||
| e42af06609 | |||
| 0416816329 | |||
| 7f8f216263 | |||
| 69ebcf08fc | |||
| cbdb007b26 | |||
| a28c03a2f9 | |||
| 94a8915f6c | |||
| be7192082a | |||
| 54297cacd1 | |||
| 82f5997e61 | |||
| 4d2bc88305 | |||
| cd8dfa331a | |||
| 45d22c6196 | |||
| eeed11e283 | |||
| 476333b3fc | |||
| c7fdbd1c64 | |||
| 0b9bc2a7a7 | |||
| 6949b6666e | |||
| a380b6803a | |||
| 6696c712d3 | |||
| 0d4833d6e3 | |||
| 207bce61ad | |||
| 0f1d367b80 | |||
| bf2e6a33c2 | |||
| b66fed9ae9 | |||
| 4897287fda | |||
| cc1daa5a54 | |||
| 6e7b9472be | |||
| e13ed6436e | |||
| db24690d9b | |||
| 3a39fd23c4 | |||
| d471277031 | |||
| ef57b88343 | |||
| 1e3fcdc109 | |||
| 606b9718a3 | |||
| ab6e30da28 | |||
| 0baea5c1a6 | |||
| bd07310e15 | |||
| bf227508ce | |||
| 1c1ba58579 | |||
| a73f898bb9 | |||
| ee8a52d0c0 | |||
| d5e87a537c | |||
| 18d3786676 | |||
| c906dbad75 | |||
| db20fc7831 | |||
| bd226d94d8 | |||
| 8a487ca1bc | |||
| 7b43a34860 | |||
| 6a60585123 | |||
| 0f7ab32777 | |||
| ffeaf2dec0 | |||
| 3277820381 | |||
| 80d0aadbd0 | |||
| d744cecbfa | |||
| d453813db4 | |||
| c06ebd1e4a | |||
| 10af2d0560 | |||
| 42f2dafb40 | |||
| 6ca7661510 | |||
| 4388cc207f | |||
| 07f14538e3 | |||
| 3d9173a877 | |||
| cb88f53587 | |||
| d76e8be4ff | |||
| e8c6002d08 | |||
| d9033812a2 | |||
| 2e6b93f886 | |||
| afc4e145b6 | |||
| cee243a2a2 | |||
| 5fd74109ff | |||
| a3cc8eb1f6 | |||
| bd4de4832c | |||
| 9e74c934a1 | |||
| a056d4916a | |||
| 31630859a2 | |||
| 8cb41f6797 | |||
| c3a8aeca42 | |||
| eaa95fb1e5 | |||
| 8e6bca34d7 | |||
| 1cdffa2c81 | |||
| 0e4eebbb39 | |||
| 8441589ce6 | |||
| b52ba89cec | |||
| b99e1205c4 | |||
| 8d502743a5 | |||
| 732a764ec6 | |||
| 9975786bac | |||
| 89ef4aa6e7 | |||
| 7e82ac3620 | |||
| c3440c506c | |||
| f16ef93cc3 | |||
| b6f3fc5466 | |||
| 6690f59410 | |||
| 65e08b9633 | |||
| 2e916e63f5 | |||
| f531b9fb21 | |||
| 9581e48bcb | |||
| ad9d58ebc2 | |||
| 896fc5a3f0 | |||
| 94addb6315 | |||
| 2c6d4c5a5c | |||
| f81d6b6157 | |||
| ef17214ae2 | |||
| 5d544c773d | |||
| 721b9df35d | |||
| 526fbfa8f1 | |||
| 0317830b12 | |||
| dfd8c56838 | |||
| b7e33b237b | |||
| 025cb8bd91 | |||
| fa89f2be77 | |||
| bf008a1bee | |||
| 8656ad584b | |||
| e316a9a5b3 | |||
| 7e3a146240 | |||
| 2395d2856b | |||
| 34ae473e6f | |||
| 656c54ead9 | |||
| c6e21c9c5c | |||
| e71e32122c | |||
| 522105a858 | |||
| e3b008e19a | |||
| 776cfed2b3 | |||
| 85cf2a3692 | |||
| c9b700ef6a | |||
| 34fde7d16d | |||
| 5911c4d2db | |||
| dfae72e9af | |||
| 085493d580 | |||
| 5245c7f2ab | |||
| 4ccd649358 | |||
| 32f42d59b1 | |||
| ca618f2bf2 | |||
| a0ae0b3922 | |||
| a09329949a | |||
| b67e360302 | |||
| 3d30ad843f | |||
| d37935dd78 | |||
| 512d5882c9 | |||
| 247deacbb7 | |||
| e79926db6c | |||
| 34a0bd4c38 | |||
| fb820fa9a7 | |||
| 423175f539 | |||
| 007848c42e | |||
| 49e6fd3c60 | |||
| 80129e7483 | |||
| dc74a2326f | |||
| fc0117869d | |||
| 7bca05af64 | |||
| 9b354ba99e | |||
| 194fad7445 | |||
| 18001dc539 | |||
| 78031f2c04 | |||
| 07261fa5d9 | |||
| aa4ffc7bda | |||
| cee7f7a280 | |||
| c7f173bbb2 | |||
| 118bd75a68 | |||
| 9a593f147f | |||
| dfcbdeb002 | |||
| df20365a6d | |||
| 9c6973a46a | |||
| b439be8fb5 | |||
| edaf84a78f | |||
| 056ed3b3c4 | |||
| 317898d41c | |||
| c8b26eeac4 | |||
| 766d8f0ba4 | |||
| e159e504fa | |||
| 8bcb048f53 | |||
| 0fa9f7c609 | |||
| c24b580165 | |||
| cc39ede920 | |||
| 7a4ef7b257 | |||
| 8c52870e07 | |||
| 922dd6cf9c | |||
| 9633532dd4 | |||
| 5abf6b9f20 | |||
| 478550ec93 | |||
| bb7c9b2227 | |||
| 8538d7881a | |||
| 5f28bc4468 | |||
| 7ed65407e6 | |||
| 97e421306b | |||
| 1ce9e7c6bb | |||
| df4ae597a5 | |||
| 8c512bce9e | |||
| 4775010452 | |||
| 03f4b15c61 | |||
| d8c23c0dcb | |||
| 4ab261b89f | |||
| e057956ede | |||
| 543b9cf0ce | |||
| 591b56d794 | |||
| 7f8375d864 | |||
| 31af4bbeb5 | |||
| 0a11404be2 | |||
| ff723980ac | |||
| 0dfd60ad5e | |||
| 18f57a2100 | |||
| 9b5cb3a631 | |||
| 09e4e4709f | |||
| 00895f00e6 | |||
| c57be7b966 | |||
| 51d94e63f4 | |||
| 6daeec838f | |||
| 53f23939c1 | |||
| 0bbec9e182 | |||
| 101970dcd9 | |||
| 6644151d19 | |||
| 548ffdced1 | |||
| cba4b24b23 | |||
| 3e922c2d41 | |||
| ae6a409cc2 | |||
| 94d79edbd0 | |||
| 4dc331d629 | |||
| 18131735d7 | |||
| 2a51e7a665 | |||
| df7ac77113 | |||
| 1b222249c4 | |||
| 126967cb90 | |||
| 2afa381cae | |||
| 05cbc217a0 | |||
| 54eec40d20 | |||
| 3ab34f911b | |||
| d6e4d0a417 | |||
| fac40f5183 | |||
| ce684a6628 | |||
| 14fac241f7 | |||
| 335579e250 | |||
| c8565be3a5 | |||
| 76e76269cf | |||
| 3c43e2718d | |||
| f2676772c8 | |||
| c9bf4270fc | |||
| 41ddb7660b | |||
| b3e93ffadf | |||
| 582576b1c3 | |||
| 456135a6ec | |||
| c7357952ec | |||
| b796246d9d | |||
| 598e48e39b | |||
| 2e3c349c5e | |||
| 7f4ff352e8 | |||
| b11bff5a5b | |||
| 223bd459f6 | |||
| ed1673c66c | |||
| 0fda43b603 | |||
| d3b63c592e | |||
| a32af7d77c | |||
| 742d942baa | |||
| 73e86bfc5d | |||
| 0a4c41c958 | |||
| 79a699f0be | |||
| 4a2e6a826b | |||
| 95f56f95ec | |||
| 67e75fb7af | |||
| ebda89d1ae | |||
| 5ce5299651 | |||
| 41bd518182 | |||
| 739e94302d | |||
| e54541aecf | |||
| 338c707579 | |||
| 301ab01911 | |||
| 99089c0f5f | |||
| ec124847d7 | |||
| 89ced19874 | |||
| f997b4a1d5 | |||
| def99728e7 | |||
| fab234dccb | |||
| 0c0572948e | |||
| a4e281265f | |||
| ce71de0d78 | |||
| c5afcaeaf7 | |||
| 889bfce65d | |||
| 1f33d76e87 | |||
| d8de23228f | |||
| 579218aafc | |||
| aefdacc566 | |||
| d619495136 | |||
| 036d1da013 | |||
| 4ba8e7e072 | |||
| f6830992ea | |||
| 769a0cb76f | |||
| 766e837775 | |||
| 749f53a22b | |||
| 851b33aac2 | |||
| fc958a3922 | |||
| 2c31b72c52 | |||
| 8decb02027 | |||
| a0fd87c032 | |||
| c0d862c9f0 | |||
| 8143abc9e7 | |||
| af95dcaef6 | |||
| 5b4aedd4be | |||
| d8c0b16d7e | |||
| 909b56d48e | |||
| a5d857945a | |||
| 1a03e534bd | |||
| e623b539c4 | |||
| 2ff6f5f958 | |||
| 1532188d95 | |||
| 04093692c9 | |||
| a96389a3e4 | |||
| 00e7c84a93 | |||
| e0c924870d | |||
| 1a27ad22a7 | |||
| 7029083266 | |||
| a5f0ec7c7d | |||
| e7dcc06855 | |||
| 867ac49b50 | |||
| bfffbea4a0 | |||
| f8b1c124df | |||
| bc9e290c11 | |||
| 777ef83378 | |||
| 24283dcbd5 | |||
| 2113c83679 | |||
| 77508f38bb | |||
| 6c3eb19b74 | |||
| e173d822e8 | |||
| e2d3ace476 | |||
| 6f79a3107b | |||
| e6a3b2aa28 | |||
| e5dcfdf115 | |||
| 47e12fcc3e | |||
| d51b2884da | |||
| 28e9c10ded | |||
| e27bf04ced | |||
| 65f1b3c976 | |||
| 4529578cd6 | |||
| 6769c96942 | |||
| dcb987732c | |||
| 4f4eba16d6 | |||
| 9da913f5a6 | |||
| a15aa0f7a4 | |||
| efa1eee6e2 | |||
| 55179f0a1a | |||
| 01593d1a69 | |||
| 40e22cfa86 | |||
| 97aeaec8d2 | |||
| 0b9f85d97b | |||
| d266486581 | |||
| c47d2fc750 | |||
| 66c4c8882f | |||
| 72d7dd7690 | |||
| fff354669c | |||
| 07ae4b0be6 | |||
| cc51805c39 | |||
| c61ac2a845 | |||
| 6d67de06a2 | |||
| ec1273893f | |||
| 1e26077d58 | |||
| ad67f002e3 | |||
| 572df32dca | |||
| 6b8181c06f | |||
| 5900542cfb | |||
| a28b825c4d | |||
| d105854619 | |||
| a4f192bc88 | |||
| db925d7fde | |||
| 16b4865035 | |||
| 20b310484b | |||
| 611a191b0e | |||
| 8b856b9d15 | |||
| 3f7df0d15c | |||
| e0917d3c47 | |||
| 19c257703c | |||
| 62b6262534 | |||
| 55bd3ac302 | |||
| 7a7f345f28 | |||
| ff2282a41a | |||
| b5c7c700d5 | |||
| de6330fb80 | |||
| aafb1ffdef | |||
| c5d738d25c | |||
| 15d8252909 | |||
| 8189c58fc3 | |||
| b3e7f4ea21 | |||
| 9c9ae562ec | |||
| f93eea095e | |||
| 09255a52f7 | |||
| 6f9c8c3007 | |||
| 2e99d5da64 | |||
| 72c8586fad | |||
| d98867b810 | |||
| de7061184b | |||
| 4e2483b41a | |||
| d3db4ee63d | |||
| 5d049cc5e8 | |||
| 6218bad00f | |||
| 2968e9c0c7 | |||
| e73051b230 | |||
| acad3e69dd | |||
| 4794dfc17b | |||
| d505ab9eeb | |||
| 631eeb9bc0 | |||
| 892ca56808 | |||
| 828c7ba451 | |||
| a3d86c03b1 | |||
| 74d6cb802f | |||
| 1b83f66536 | |||
| e5d5cd901a | |||
| 92ae4dda72 | |||
| cd5a88c718 | |||
| 1c744a66e6 | |||
| 57cf7e1f7d | |||
| 86ea00cfee | |||
| 02f8e7da3d | |||
| a245b735b3 | |||
| 0f71983cb9 | |||
| 7db6b9e490 | |||
| 0021b21170 | |||
| 3080dc018a | |||
| f5d0ec32e5 | |||
| 6b3a06a8ed | |||
| 9e89e71e0e | |||
| dcedc78fc2 | |||
| faff057592 | |||
| 56be271b0a | |||
| fa557eb0cc | |||
| b784d1a5e7 | |||
| f6614ac0e4 | |||
| e4aea701ab | |||
| 352f79e9fd | |||
| 3a17ef983e | |||
| 91e571fb68 | |||
| 1a3ee28d01 | |||
| 669aecf4e6 | |||
| ea23db6450 | |||
| 69e4bdd421 | |||
| 86a4fd687c | |||
| 14bc4af90c | |||
| 0cd2b2c0e2 | |||
| 832559926f | |||
| 59411353b1 | |||
| 83bd420cd5 | |||
| 78a0aa5d47 | |||
| 6e31319294 | |||
| cd0b19f93f | |||
| 4f22610499 | |||
| 9e57a9352a | |||
| 4e0d7b56d8 | |||
| f2e10e030d | |||
| 266b7afc72 | |||
| a15dffbb3a | |||
| a30c816cb6 | |||
| e65fe483e1 | |||
| 0d51fad805 | |||
| 17ed38ad05 | |||
| 8259f08882 | |||
| 55d6cf7ab0 | |||
| 425f862cf8 | |||
| 5d6256bede | |||
| ff5b923e6f | |||
| af7a9a68b8 | |||
| 905059d6da | |||
| 3bc56cf3f8 | |||
| 1feb7fc0ba | |||
| c2a40572a5 | |||
| ee7d4d0521 | |||
| 6ab410ef6a | |||
| 8235d966d6 | |||
| c7b83f6ee6 | |||
| 460f20a4ce | |||
| da408f975e | |||
| 9a98c3991a | |||
| 6e0b2de99f | |||
| 0633d7d3f6 | |||
| 2765720b76 | |||
| 71f23ffce1 | |||
| 1863af147d | |||
| 0d5d74674e | |||
| 45ed0884df | |||
| 45e9f59fdc | |||
| bde6a171f6 | |||
| 49a74755a8 | |||
| 2fbef8638f | |||
| eb4166afe3 | |||
| b3beaacec7 | |||
| 355b728a57 | |||
| 577b0e8f1b | |||
| 35d99564c1 | |||
| 6f9bb38232 | |||
| d02c205910 | |||
| 38681202dc | |||
| 0d20a0acf0 | |||
| 9277a86403 | |||
| 5ec8688cf6 | |||
| 6ae82a9cb4 | |||
| 72a4b92022 | |||
| 0cc68bc125 | |||
| 6ca917f4db | |||
| 8a848deddc | |||
| 2ebd4b15a4 | |||
| f0274f3f26 | |||
| 85b2e5d758 | |||
| eef03882ad | |||
| f7e5d962c0 | |||
| 87c6a40b3f | |||
| e614e17a71 | |||
| b4dc5e620b | |||
| 0713e65fc5 | |||
| b69f6cf70a | |||
| 2c6409a67a | |||
| ad7db78829 | |||
| bd9e3e5794 | |||
| bd32ed5598 | |||
| 5a5257a598 | |||
| 75b6ebf287 | |||
| a9d3ae4ef8 | |||
| d480b6cf3e | |||
| fdb640e361 | |||
| 924a8533f1 | |||
| 72b4f270ff | |||
| 946539e32d | |||
| 9882fed6d7 | |||
| 93f45c0a94 | |||
| c6d358a6f3 | |||
| 2e4c362ccd | |||
| f959e1a134 | |||
| 7dfc4a404c | |||
| 2af349eb72 | |||
| 43f3a1e8b3 | |||
| 13c186dfbe | |||
| 4d88736d13 | |||
| 1da633e28a | |||
| 879da47f0e | |||
| cacafb461d | |||
| 15e285c6b4 | |||
| 71c33420f6 | |||
| e7f70bba5c | |||
| e4ec2aa55f | |||
| fc495a5f1e | |||
| 6fe4dfcad0 | |||
| dac820f957 | |||
| 91f8df8d19 | |||
| 9b507f6c6c | |||
| 5e583d3c50 | |||
| d706b57fe9 | |||
| 1063a16013 | |||
| 46a2073427 | |||
| d7bb9574e7 | |||
| 9c18893ae5 | |||
| 4503c320e5 | |||
| c4995bd153 | |||
| af0f5b37d8 | |||
| b9ba4671b4 | |||
| 50b8f13037 | |||
| 4aa9dca608 | |||
| 98dc5328a0 | |||
| 408671b58a | |||
| 1bda527e3d | |||
| e0f1b9ebf0 | |||
| 55127aa43f | |||
| 888fbe3549 | |||
| ed5c061566 | |||
| df6b1d1471 | |||
| 5e0f09075d | |||
| 2daa1b6007 | |||
| 4ff2ad9fac | |||
| ba06e8091f | |||
| 692b3107ac | |||
| 6baf9e1c37 | |||
| c07e662b90 | |||
| aca8b32e5b | |||
| 0ec8a6e0af | |||
| 41af7c8883 | |||
| 7f2070f7b7 | |||
| 1cc74ec116 | |||
| 627f662384 | |||
| c791881c87 | |||
| b0782885d5 | |||
| d25d60f0f0 | |||
| d356e722da | |||
| 84e2fc91ae | |||
| 9768fb020c | |||
| e25112ad35 | |||
| e18b446190 | |||
| 0848d4ed10 | |||
| 7514aea813 | |||
| 58031ab21d | |||
| c1c2ca3ec1 | |||
| b863a363da | |||
| b42db46abd | |||
| d46863e199 | |||
| 751ce421cd | |||
| 74e1dccf50 | |||
| bf3eaa9eb7 | |||
| b4f22310ea | |||
| 2da70ca024 | |||
| 42babbc595 | |||
| ba4735d4a8 | |||
| 31e7addf2f | |||
| ba339ffdad | |||
| a05cbab7c6 | |||
| e708e59b15 | |||
| dd5878015a | |||
| c72f613afc | |||
| 1159e0911f | |||
| 9f180179d5 | |||
| 238700cbdb | |||
| df43b19510 | |||
| e4bfb3ca32 | |||
| 0234410b43 | |||
| 4877edb79b | |||
| 1c4ee62397 | |||
| 7ea7e5ac6c | |||
| 32fa51818b | |||
| e0bd05a8c4 | |||
| a25315a994 | |||
| 03e493453b | |||
| 4d6f9da578 | |||
| 783b1feb70 | |||
| 9925b327b4 | |||
| f75287b6b9 | |||
| cc72d35c6b | |||
| 89d8133ad2 | |||
| e56833c7b2 | |||
| 2c9f8ba598 | |||
| 1f16bba342 | |||
| 4cde51b3ce | |||
| 267e009ae3 | |||
| 0ba1a1dabc | |||
| 6739da5acb | |||
| b0d5e1d844 | |||
| ea6f526ef9 | |||
| 3a7b1c6dd4 | |||
| b98e421b8a | |||
| d9318a60e4 | |||
| 2fd569a61a | |||
| 1bd5d12665 | |||
| 02de5e96ba | |||
| bc56213010 | |||
| 34919d1b96 | |||
| cc08de9c64 | |||
| 6432a64442 | |||
| 1f18dabca0 | |||
| f74b49de4b | |||
| 6aeb265c19 | |||
| f10467e81f | |||
| 6001077c34 | |||
| ad6eec329d | |||
| d9867ba458 | |||
| 6dc7e624d3 | |||
| 24957a1445 | |||
| e2d67db5d4 | |||
| 6c59966339 | |||
| b4223d3790 | |||
| 9f6d9208f2 | |||
| 35d45f0280 | |||
| c288e6c7ec | |||
| bb946c65d1 | |||
| 13fe22bc86 | |||
| f139e6e6c2 | |||
| 364fe3ba47 | |||
| 8b9b37cc91 | |||
| 7895d1daa0 | |||
| 93a9f76f69 | |||
| 4e2edfc771 | |||
| f4d53e25cc | |||
| da324c020b | |||
| f63015e4c4 | |||
| 61cf53deee | |||
| 57ff963fae | |||
| 32744b23a0 | |||
| 6c25110682 | |||
| 188802c5d3 | |||
| a47e59f02c | |||
| 59d7935934 | |||
| ba616d2a25 | |||
| dc07038a27 | |||
| dd064ba0a1 | |||
| 3baea89c34 | |||
| 1412646a55 | |||
| c00a830cbb | |||
| fa28297add | |||
| 58a68106bc | |||
| ebd2ef6f95 | |||
| 809492d45d | |||
| 385c5d5469 | |||
| 9713ffedf2 | |||
| ecb31b5aaf | |||
| cee9a954ec | |||
| fc55858aa3 | |||
| b689dbb9c0 | |||
| 7dbc03942a | |||
| 425039c5b5 | |||
| 03d0aecc26 | |||
| abf903246c | |||
| 3fd601bda4 | |||
| 1896c0b62f | |||
| aa36571981 | |||
| abbe9d2bc7 | |||
| dff84c46f0 | |||
| 7614c6677c | |||
| 3d2a970457 | |||
| 50d60f5dd3 | |||
| 90c919e7e4 | |||
| 583ddc3e57 | |||
| 7ff1cf4e4a | |||
| 1556cc4479 | |||
| 32f7ca55df | |||
| ae3551ad78 | |||
| d0e90cd8c9 | |||
| cb6566a198 | |||
| 573a9140d3 | |||
| 6a02f39f0b | |||
| 7604bcc49a | |||
| 40c73e2079 | |||
| 7f113de790 | |||
| accb589892 | |||
| 4347f432b3 | |||
| 6d905563fc | |||
| e943bc46d8 | |||
| 7896b06bd7 | |||
| 0f153fdaf7 | |||
| 2e4a8f4fa5 | |||
| 53b9154fe2 | |||
| ce59b72308 | |||
| 4bf24f0fe4 | |||
| 23a38ae8f2 | |||
| 0ab3446d81 | |||
| adaf4bf92b | |||
| 60519c4e6b | |||
| 21a62f37de | |||
| 9feeb0c580 | |||
| 7c3104b2ae | |||
| 5ccbc0bbc6 | |||
| 9c47400a0c | |||
| 9cb032ec44 | |||
| 8c6e2591d9 | |||
| 0c8c7cf77a | |||
| 52edcc49c5 | |||
| 5eede573c4 | |||
| e9d60a252b | |||
| 17ec7daf23 | |||
| b18a4ee16b | |||
| f38f983a46 |
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"sourceMaps": true,
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"node": 10
|
||||
},
|
||||
"modules": "commonjs"
|
||||
}],
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-numeric-separator",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-transform-runtime"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copyright 2017 Aviral Dasgupta
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset=utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
@@ -0,0 +1,65 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
"matrix-org",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/babel",
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
// NOTE: These rules are frozen and new rules should not be added here.
|
||||
// New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
|
||||
rules: {
|
||||
"no-var": ["warn"],
|
||||
"prefer-rest-params": ["warn"],
|
||||
"prefer-spread": ["warn"],
|
||||
"one-var": ["warn"],
|
||||
"padded-blocks": ["warn"],
|
||||
"no-extend-native": ["warn"],
|
||||
"camelcase": ["warn"],
|
||||
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
|
||||
"space-before-function-paren": ["error", {
|
||||
"anonymous": "never",
|
||||
"named": "never",
|
||||
"asyncArrow": "always",
|
||||
}],
|
||||
"arrow-parens": "off",
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"quotes": "off",
|
||||
"indent": "off",
|
||||
"no-constant-condition": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
|
||||
// restrict EventEmitters to force callers to use TypedEventEmitter
|
||||
"no-restricted-imports": ["error", "events"],
|
||||
},
|
||||
overrides: [{
|
||||
files: [
|
||||
"**/*.ts",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/typescript",
|
||||
],
|
||||
rules: {
|
||||
// TypeScript has its own version of this
|
||||
"@babel/no-invalid-this": "off",
|
||||
|
||||
// We're okay being explicit at the moment
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
// We disable this while we're transitioning
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// We'd rather not do this but we do
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
|
||||
"quotes": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
},
|
||||
}],
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
# Minor white-space adjustments
|
||||
1d1d59c75744e1f6a2be1cb3e0d1bd9ded5f8025
|
||||
# Import ordering and spacing: eslint-plugin-import
|
||||
80aaa6c32b50601f82e0c991c24e5a4590f39463
|
||||
# Minor white-space adjustment
|
||||
8fb036ba2d01fab66dc4373802ccf19b5cac8541
|
||||
# Minor white-space adjustment
|
||||
b63de6a902a9e1f8ffd7697dea33820fc04f028e
|
||||
3ca84cfc491b0987eec1f13f13cae58d2032bf54
|
||||
# Conform to new typescript eslint rules
|
||||
a87858840b57514603f63e2abbbda4f107f05a77
|
||||
5cf6684129a921295f5593173f16f192336fe0a2
|
||||
# Comply with new member-delimiter-style rule
|
||||
b2ad957d298720d3e026b6bd91be0c403338361a
|
||||
# Fix semicolons in TS files
|
||||
e2ec8952e38b8fea3f0ccaa09ecb42feeba0d923
|
||||
# Migrate to `eslint-plugin-matrix-org`
|
||||
# and `babel/...` to `@babel/...` migration
|
||||
09fac77ce0d9bcf6637088c29afab84084f0e739
|
||||
102704e91a70643bcc09721e14b0d909f0ef55c6
|
||||
# Eslint formatting
|
||||
cec00cd303787fa9008b6c48826e75ed438036fa
|
||||
# Minor eslint changes
|
||||
68bb8182e4e62d8f450f80c408c4b231b8725f1b
|
||||
c979ff6696e30ab8983ac416a3590996d84d3560
|
||||
f4a7395e3a3751a1a8e92dd302c49175a3296ad2
|
||||
# eslint --fix for dangley commas on function calls
|
||||
423175f5397910b0afe3112d6fb18283fc7d27d4
|
||||
# eslint ---fix for prefer-const
|
||||
7bca05af644e8b997dae81e568a3913d8f18d7ca
|
||||
# Fix linting on tests
|
||||
cee7f7a280a8c20bafc21c0a2911f60851f7a7ca
|
||||
# eslint --fix
|
||||
0fa9f7c6098822db1ae214f352fd1fe5c248b02c
|
||||
# eslint --fix for lots of white-space
|
||||
5abf6b9f208801c5022a47023150b5846cb0b309
|
||||
# eslint --fix
|
||||
7ed65407e6cdf292ce3cf659310c68d19dcd52b2
|
||||
# Switch to ESLint from JSHint (Google eslint rules as a base)
|
||||
e057956ede9ad1a931ff8050c411aca7907e0394
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
* @matrix-org/element-web
|
||||
@@ -0,0 +1,2 @@
|
||||
patreon: matrixdotorg
|
||||
liberapay: matrixdotorg
|
||||
@@ -0,0 +1,13 @@
|
||||
<!-- Thanks for submitting a PR! Please ensure the following requirements are met in order for us to review your PR -->
|
||||
|
||||
## Checklist
|
||||
|
||||
* [ ] Tests written for new code (and old code if feasible)
|
||||
* [ ] Linter and other CI checks pass
|
||||
* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md))
|
||||
|
||||
<!--
|
||||
If you would like to specify text for the changelog entry other than your PR title, add the following:
|
||||
|
||||
Notes: Add super cool feature
|
||||
-->
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"github>matrix-org/renovate-config-element-web"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Release Process
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
jsdoc:
|
||||
name: Publish Documentation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🧮 Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🔧 Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: 🔨 Install dependencies
|
||||
run: "yarn install --pure-lockfile"
|
||||
|
||||
- name: 📖 Generate JSDoc
|
||||
run: "yarn gendoc"
|
||||
|
||||
- name: 📋 Copy to temp
|
||||
run: |
|
||||
ls -lah
|
||||
tag="${{ github.ref_name }}"
|
||||
version="${tag#v}"
|
||||
echo "VERSION=$version" >> $GITHUB_ENV
|
||||
cp -a "./.jsdoc/matrix-js-sdk/$version" $RUNNER_TEMP
|
||||
|
||||
- name: 🧮 Checkout gh-pages
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: gh-pages
|
||||
|
||||
- name: 🔪 Prepare
|
||||
run: |
|
||||
cp -a "$RUNNER_TEMP/$VERSION" .
|
||||
|
||||
# Add the new directory to the index if it isn't there already
|
||||
if ! grep -q "Version $VERSION" index.html; then
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' "$VERSION" index.html
|
||||
fi
|
||||
|
||||
- name: 🚀 Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
keep_files: true
|
||||
publish_dir: .
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Notify Downstream Projects
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
notify-downstream:
|
||||
# Only respect triggers from our develop branch, ignore that of forks
|
||||
if: github.repository == 'matrix-org/matrix-js-sdk'
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- repo: vector-im/element-web
|
||||
event: element-web-notify
|
||||
- repo: matrix-org/matrix-react-sdk
|
||||
event: upstream-sdk-notify
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: ${{ matrix.repo }}
|
||||
event-type: ${{ matrix.event }}
|
||||
@@ -0,0 +1,91 @@
|
||||
name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened, edited, labeled, unlabeled, synchronize ]
|
||||
workflow_call:
|
||||
inputs:
|
||||
labels:
|
||||
type: string
|
||||
default: "T-Defect,T-Deprecation,T-Enhancement,T-Task"
|
||||
required: false
|
||||
description: "No longer used, uses allchange logic now, will be removed at a later date"
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
|
||||
jobs:
|
||||
changelog:
|
||||
name: Preview Changelog
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: matrix-org/allchange@main
|
||||
with:
|
||||
ghToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
requireLabel: true
|
||||
|
||||
prevent-blocked:
|
||||
name: Prevent Blocked
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Add notice
|
||||
uses: actions/github-script@v6
|
||||
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
|
||||
with:
|
||||
script: |
|
||||
core.setFailed("Preventing merge whilst PR is marked blocked!");
|
||||
|
||||
community-prs:
|
||||
name: Label Community PRs
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Check membership
|
||||
uses: tspascoal/get-user-teams-membership@v1
|
||||
id: teams
|
||||
with:
|
||||
username: ${{ github.event.pull_request.user.login }}
|
||||
organization: matrix-org
|
||||
team: Core Team
|
||||
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
- name: Add label
|
||||
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['Z-Community-PR']
|
||||
});
|
||||
|
||||
close-if-fork-develop:
|
||||
name: Forbid develop branch fork contributions
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.head.ref == 'develop' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository
|
||||
steps:
|
||||
- name: Close pull request
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Thanks for opening this pull request, unfortunately we do not accept contributions from the main" +
|
||||
" branch of your fork, please re-open once you switch to an alternative branch for everyone's sanity." +
|
||||
" See https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md",
|
||||
});
|
||||
|
||||
github.rest.pulls.update({
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'closed'
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
# Must only be called from a workflow_run in the context of the upstream repo
|
||||
name: SonarCloud
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
required: true
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
uses: matrix-org/sonarcloud-workflow-action@v2.2
|
||||
with:
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
version_cmd: 'cat package.json | jq -r .version'
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
coverage_run_id: ${{ github.event.workflow_run.id }}
|
||||
coverage_workflow_name: tests.yml
|
||||
coverage_extract_path: coverage
|
||||
@@ -0,0 +1,15 @@
|
||||
name: SonarQube
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Tests" ]
|
||||
types:
|
||||
- completed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
@@ -0,0 +1,56 @@
|
||||
name: Static Analysis
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
|
||||
docs:
|
||||
name: "JSDoc Checker"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Generate Docs
|
||||
run: "yarn run gendoc"
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Tests
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
jest:
|
||||
name: Jest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
|
||||
- name: Build
|
||||
run: "yarn build"
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: "yarn coverage --ci --reporters github-actions"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Upgrade Dependencies
|
||||
on:
|
||||
workflow_dispatch: { }
|
||||
workflow_call:
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
jobs:
|
||||
upgrade:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Upgrade
|
||||
run: yarn upgrade && yarn install
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/upgrade-deps
|
||||
delete-branch: true
|
||||
title: Upgrade dependencies
|
||||
labels: |
|
||||
Dependencies
|
||||
T-Task
|
||||
|
||||
- name: Enable automerge
|
||||
uses: peter-evans/enable-pull-request-automerge@v2
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
+11
-5
@@ -1,15 +1,21 @@
|
||||
# Keep this file in sync with .npmignore.
|
||||
/.jsdocbuild
|
||||
/.jsdoc
|
||||
|
||||
.jsdoc
|
||||
node_modules
|
||||
/.npmrc
|
||||
/*.log
|
||||
package-lock.json
|
||||
.lock-wscript
|
||||
build/Release
|
||||
coverage
|
||||
lib-cov
|
||||
out
|
||||
reports
|
||||
dist/browser-matrix-dev.js
|
||||
/dist
|
||||
/lib
|
||||
|
||||
# version file and tarball created by 'npm pack'
|
||||
# version file and tarball created by `npm pack` / `yarn pack`
|
||||
/git-revision.txt
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
.vscode
|
||||
.vscode/
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
instrumentation:
|
||||
compact: false
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"node": true,
|
||||
"jasmine": true,
|
||||
|
||||
"nonew": true,
|
||||
"curly": true,
|
||||
"forin": true,
|
||||
"freeze": false,
|
||||
"undef": true,
|
||||
"unused": "vars"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
# Keep this file in sync with .gitignore.
|
||||
|
||||
.jsdoc
|
||||
node_modules
|
||||
.lock-wscript
|
||||
build/Release
|
||||
coverage
|
||||
lib-cov
|
||||
out
|
||||
reports
|
||||
dist/browser-matrix-dev.js
|
||||
|
||||
# tarball created by 'npm pack'.
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
+4361
File diff suppressed because it is too large
Load Diff
+284
@@ -0,0 +1,284 @@
|
||||
Contributing code to matrix-js-sdk
|
||||
==================================
|
||||
|
||||
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
|
||||
willing to license their contributions under the same license as the project
|
||||
itself. We follow a simple 'inbound=outbound' model for contributions: the act
|
||||
of submitting an 'inbound' contribution means that the contributor agrees to
|
||||
license the code under the same terms as the project's overall 'outbound'
|
||||
license - in this case, Apache Software License v2 (see
|
||||
[LICENSE](LICENSE)).
|
||||
|
||||
How to contribute
|
||||
-----------------
|
||||
|
||||
The preferred and easiest way to contribute changes to the project is to fork
|
||||
it on github, and then create a pull request to ask us to pull your changes
|
||||
into our repo (https://help.github.com/articles/using-pull-requests/)
|
||||
|
||||
We use GitHub's pull request workflow to review the contribution, and either
|
||||
ask you to make any refinements needed or merge it and make them ourselves.
|
||||
|
||||
Things that should go into your PR description:
|
||||
* A changelog entry in the `Notes` section (see below)
|
||||
* References to any bugs fixed by the change (in GitHub's `Fixes` notation)
|
||||
* Describe the why and what is changing in the PR description so it's easy for
|
||||
onlookers and reviewers to onboard and context switch. This information is
|
||||
also helpful when we come back to look at this in 6 months and ask "why did
|
||||
we do it like that?" we have a chance of finding out.
|
||||
* Why didn't it work before? Why does it work now? What use cases does it
|
||||
unlock?
|
||||
* If you find yourself adding information on how the code works or why you
|
||||
chose to do it the way you did, make sure this information is instead
|
||||
written as comments in the code itself.
|
||||
* Sometimes a PR can change considerably as it is developed. In this case,
|
||||
the description should be updated to reflect the most recent state of
|
||||
the PR. (It can be helpful to retain the old content under a suitable
|
||||
heading, for additional context.)
|
||||
* Include both **before** and **after** screenshots to easily compare and discuss
|
||||
what's changing.
|
||||
* Include a step-by-step testing strategy so that a reviewer can check out the
|
||||
code locally and easily get to the point of testing your change.
|
||||
* Add comments to the diff for the reviewer that might help them to understand
|
||||
why the change is necessary or how they might better understand and review it.
|
||||
|
||||
We rely on information in pull request to populate the information that goes
|
||||
into the changelogs our users see, both for the JS SDK itself and also for some
|
||||
projects based on it. This is picked up from both labels on the pull request and
|
||||
the `Notes:` annotation in the description. By default, the PR title will be
|
||||
used for the changelog entry, but you can specify more options, as follows.
|
||||
|
||||
To add a longer, more detailed description of the change for the changelog:
|
||||
|
||||
|
||||
*Fix llama herding bug*
|
||||
|
||||
```
|
||||
Notes: Fix a bug (https://github.com/matrix-org/notaproject/issues/123) where the 'Herd' button would not herd more than 8 Llamas if the moon was in the waxing gibbous phase
|
||||
```
|
||||
|
||||
For some PRs, it's not useful to have an entry in the user-facing changelog (this is
|
||||
the default for PRs labelled with `T-Task`):
|
||||
|
||||
*Remove outdated comment from `Ungulates.ts`*
|
||||
```
|
||||
Notes: none
|
||||
```
|
||||
|
||||
Sometimes, you're fixing a bug in a downstream project, in which case you want
|
||||
an entry in that project's changelog. You can do that too:
|
||||
|
||||
*Fix another herding bug*
|
||||
```
|
||||
Notes: Fix a bug where the `herd()` function would only work on Tuesdays
|
||||
element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
|
||||
```
|
||||
|
||||
This example is for Element Web. You can specify:
|
||||
* matrix-react-sdk
|
||||
* element-web
|
||||
* element-desktop
|
||||
|
||||
If your PR introduces a breaking change, use the `Notes` section in the same
|
||||
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
|
||||
to specify in the notes that it's a breaking change - this will be added
|
||||
automatically based on the label - but remember to tell the developer how to
|
||||
migrate:
|
||||
|
||||
*Remove legacy class*
|
||||
|
||||
```
|
||||
Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
|
||||
```
|
||||
|
||||
Other metadata can be added using labels.
|
||||
* `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a *major* version bump.
|
||||
* `T-Enhancement`: A new feature - adding this label will mean the change causes a *minor* version bump.
|
||||
* `T-Defect`: A bug fix (in either code or docs).
|
||||
* `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
|
||||
|
||||
If you don't have permission to add labels, your PR reviewer(s) can work with you
|
||||
to add them: ask in the PR description or comments.
|
||||
|
||||
We use continuous integration, and all pull requests get automatically tested:
|
||||
if your change breaks the build, then the PR will show that there are failed
|
||||
checks, so please check back after a few minutes.
|
||||
|
||||
Tests
|
||||
-----
|
||||
Your PR should include tests.
|
||||
|
||||
For new user facing features in `matrix-react-sdk` or `element-web`, you
|
||||
must include:
|
||||
|
||||
1. Comprehensive unit tests written in Jest. These are located in `/test`.
|
||||
2. "happy path" end-to-end tests.
|
||||
These are located in `/test/end-to-end-tests` in `matrix-react-sdk`, and
|
||||
are run using `element-web`. Ideally, you would also include tests for edge
|
||||
and error cases.
|
||||
|
||||
Unit tests are expected even when the feature is in labs. It's good practice
|
||||
to write tests alongside the code as it ensures the code is testable from
|
||||
the start, and gives you a fast feedback loop while you're developing the
|
||||
functionality. End-to-end tests should be added prior to the feature
|
||||
leaving labs, but don't have to be present from the start (although it might
|
||||
be beneficial to have some running early, so you can test things faster).
|
||||
|
||||
For bugs in those repos, your change must include at least one unit test or
|
||||
end-to-end test; which is best depends on what sort of test most concisely
|
||||
exercises the area.
|
||||
|
||||
Changes to `matrix-js-sdk` must be accompanied by unit tests written in Jest.
|
||||
These are located in `/spec/`.
|
||||
|
||||
When writing unit tests, please aim for a high level of test coverage
|
||||
for new code - 80% or greater. If you cannot achieve that, please document
|
||||
why it's not possible in your PR.
|
||||
|
||||
Some sections of code are not sensible to add coverage for, such as those
|
||||
which explicitly inhibit noisy logging for tests. Which can be hidden using
|
||||
an istanbul magic comment as [documented here][1]. See example:
|
||||
```javascript
|
||||
/* istanbul ignore if */
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
logger.error("Log line that is noisy enough in tests to want to skip");
|
||||
}
|
||||
```
|
||||
|
||||
Tests validate that your change works as intended and also document
|
||||
concisely what is being changed. Ideally, your new tests fail
|
||||
prior to your change, and succeed once it has been applied. You may
|
||||
find this simpler to achieve if you write the tests first.
|
||||
|
||||
If you're spiking some code that's experimental and not being used to support
|
||||
production features, exceptions can be made to requirements for tests.
|
||||
Note that tests will still be required in order to ship the feature, and it's
|
||||
strongly encouraged to think about tests early in the process, as adding
|
||||
tests later will become progressively more difficult.
|
||||
|
||||
If you're not sure how to approach writing tests for your change, ask for help
|
||||
in [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
|
||||
|
||||
Code style
|
||||
----------
|
||||
The js-sdk aims to target TypeScript/ES6. All new files should be written in
|
||||
TypeScript and existing files should use ES6 principles where possible.
|
||||
|
||||
Members should not be exported as a default export in general - it causes problems
|
||||
with the architecture of the SDK (index file becomes less clear) and could
|
||||
introduce naming problems (as default exports get aliased upon import). In
|
||||
general, avoid using `export default`.
|
||||
|
||||
The remaining code-style for matrix-js-sdk is not formally documented, but
|
||||
contributors are encouraged to read the
|
||||
[code style document for matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md)
|
||||
and follow the principles set out there.
|
||||
|
||||
Please ensure your changes match the cosmetic style of the existing project,
|
||||
and ***never*** mix cosmetic and functional changes in the same commit, as it
|
||||
makes it horribly hard to review otherwise.
|
||||
|
||||
Attribution
|
||||
-----------
|
||||
Everyone who contributes anything to Matrix is welcome to be listed in the
|
||||
AUTHORS.rst file for the project in question. Please feel free to include a
|
||||
change to AUTHORS.rst in your pull request to list yourself and a short
|
||||
description of the area(s) you've worked on. Also, we sometimes have swag to
|
||||
give away to contributors - if you feel that Matrix-branded apparel is missing
|
||||
from your life, please mail us your shipping address to matrix at matrix.org
|
||||
and we'll try to fix it :)
|
||||
|
||||
Sign off
|
||||
--------
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix:
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment:
|
||||
|
||||
```
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
```
|
||||
|
||||
We accept contributions under a legally identifiable name, such as your name on
|
||||
government documentation or common-law names (names claimed by legitimate usage
|
||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||
time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the `-s` flag to
|
||||
`git commit`, which uses the name and email set in your `user.name` and
|
||||
`user.email` git configs.
|
||||
|
||||
If you forgot to sign off your commits before making your pull request and are
|
||||
on Git 2.17+ you can mass signoff using rebase:
|
||||
|
||||
```
|
||||
git rebase --signoff origin/develop
|
||||
```
|
||||
|
||||
Review expectations
|
||||
===================
|
||||
|
||||
See https://github.com/vector-im/element-meta/wiki/Review-process
|
||||
|
||||
|
||||
Merge Strategy
|
||||
==============
|
||||
|
||||
The preferred method for merging pull requests is squash merging to keep the
|
||||
commit history trim, but it is up to the discretion of the team member merging
|
||||
the change. We do not support rebase merges due to `allchange` being unable to
|
||||
handle them. When merging make sure to leave the default commit title, or
|
||||
at least leave the PR number at the end in brackets like by default.
|
||||
When stacking pull requests, you may wish to do the following:
|
||||
|
||||
1. Branch from develop to your branch (branch1), push commits onto it and open a pull request
|
||||
2. Branch from your base branch (branch1) to your work branch (branch2), push commits and open a pull request configuring the base to be branch1, saying in the description that it is based on your other PR.
|
||||
3. Merge the first PR using a merge commit otherwise your stacked PR will need a rebase. Github will automatically adjust the base branch of your other PR to be develop.
|
||||
|
||||
|
||||
[1]: https://github.com/gotwarlost/istanbul/blob/master/ignoring-code-for-coverage.md
|
||||
@@ -1,8 +1,15 @@
|
||||
[](https://www.npmjs.com/package/matrix-js-sdk)
|
||||

|
||||

|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
[](https://sonarcloud.io/summary/new_code?id=matrix-js-sdk)
|
||||
|
||||
Matrix Javascript SDK
|
||||
=====================
|
||||
[](http://matrix.org/jenkins/job/JavascriptSDK/)
|
||||
|
||||
This is the [Matrix](https://matrix.org) Client-Server v1/v2 alpha SDK for
|
||||
This is the [Matrix](https://matrix.org) Client-Server r0 SDK for
|
||||
JavaScript. This SDK can be run in a browser or in Node.js.
|
||||
|
||||
Quickstart
|
||||
@@ -10,26 +17,94 @@ Quickstart
|
||||
|
||||
In a browser
|
||||
------------
|
||||
Copy ``dist/$VERSION/browser-matrix-$VERSION.js`` and add that as a ``<script>`` to
|
||||
your page. There will be a global variable ``matrixcs`` attached to
|
||||
``window`` through which you can access the SDK.
|
||||
Download the browser version from
|
||||
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
|
||||
``<script>`` to your page. There will be a global variable ``matrixcs``
|
||||
attached to ``window`` through which you can access the SDK. See below for how to
|
||||
include libolm to enable end-to-end-encryption.
|
||||
|
||||
Please check [the working browser example](examples/browser) for more information.
|
||||
The browser bundle supports recent versions of browsers. Typically this is ES2015
|
||||
or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
|
||||
[browserlists](https://github.com/browserslist/browserslist).
|
||||
|
||||
Please check [the working browser example](examples/browser) for more information.
|
||||
|
||||
In Node.js
|
||||
----------
|
||||
|
||||
``npm install matrix-js-sdk``
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
|
||||
This SDK targets Node 12 for compatibility, which translates to ES6. If you're using
|
||||
a bundler like webpack you'll likely have to transpile dependencies, including this
|
||||
SDK, to match your target browsers.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
|
||||
``yarn add matrix-js-sdk``
|
||||
|
||||
```javascript
|
||||
var sdk = require("matrix-js-sdk");
|
||||
var client = sdk.createClient("https://matrix.org");
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const client = sdk.createClient("https://matrix.org");
|
||||
client.publicRooms(function(err, data) {
|
||||
console.log("Public Rooms: %s", JSON.stringify(data));
|
||||
});
|
||||
```
|
||||
|
||||
Please check [the Node.js terminal app](examples/node) for a more complex example.
|
||||
See below for how to include libolm to enable end-to-end-encryption. Please check
|
||||
[the Node.js terminal app](examples/node) for a more complex example.
|
||||
|
||||
To start the client:
|
||||
|
||||
```javascript
|
||||
await client.startClient({initialSyncLimit: 10});
|
||||
```
|
||||
|
||||
You can perform a call to `/sync` to get the current state of the client:
|
||||
|
||||
```javascript
|
||||
client.once('sync', function(state, prevState, res) {
|
||||
if(state === 'PREPARED') {
|
||||
console.log("prepared");
|
||||
} else {
|
||||
console.log(state);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
To send a message:
|
||||
|
||||
```javascript
|
||||
const content = {
|
||||
"body": "message text",
|
||||
"msgtype": "m.text"
|
||||
};
|
||||
client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
|
||||
console.log(err);
|
||||
});
|
||||
```
|
||||
|
||||
To listen for message events:
|
||||
|
||||
```javascript
|
||||
client.on("Room.timeline", function(event, room, toStartOfTimeline) {
|
||||
if (event.getType() !== "m.room.message") {
|
||||
return; // only use messages
|
||||
}
|
||||
console.log(event.event.content.body);
|
||||
});
|
||||
```
|
||||
|
||||
By default, the `matrix-js-sdk` client uses the `MemoryStore` to store events as they are received. For example to iterate through the currently stored timeline for a room:
|
||||
|
||||
```javascript
|
||||
Object.keys(client.store.rooms).forEach((roomId) => {
|
||||
client.getRoom(roomId).timeline.forEach(t => {
|
||||
console.log(t.event);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
What does this SDK do?
|
||||
----------------------
|
||||
@@ -64,6 +139,7 @@ Later versions of the SDK will:
|
||||
Usage
|
||||
=====
|
||||
|
||||
|
||||
Conventions
|
||||
-----------
|
||||
|
||||
@@ -78,7 +154,7 @@ are updated.
|
||||
client.on("event", function(event) {
|
||||
console.log(event.getType());
|
||||
});
|
||||
|
||||
|
||||
// Listen for typing changes
|
||||
client.on("RoomMember.typing", function(event, member) {
|
||||
if (member.typing) {
|
||||
@@ -88,38 +164,41 @@ are updated.
|
||||
console.log(member.name + " stopped typing.");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// start the client to setup the connection to the server
|
||||
client.startClient();
|
||||
```
|
||||
|
||||
### Promises or Callbacks
|
||||
### Promises and Callbacks
|
||||
|
||||
The SDK supports *both* callbacks and Promises (Q). The convention
|
||||
you'll see used is:
|
||||
Most of the methods in the SDK are asynchronous: they do not directly return a
|
||||
result, but instead return a [Promise](http://documentup.com/kriskowal/q/)
|
||||
which will be fulfilled in the future.
|
||||
|
||||
The typical usage is something like:
|
||||
|
||||
```javascript
|
||||
var promise = matrixClient.someMethod(arg1, arg2, callback);
|
||||
```
|
||||
|
||||
The ``callback`` parameter is optional, so you could do:
|
||||
|
||||
```javascript
|
||||
matrixClient.someMethod(arg1, arg2).then(function(err, result) {
|
||||
matrixClient.someMethod(arg1, arg2).then(function(result) {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
Alternatively, you could do:
|
||||
Alternatively, if you have a Node.js-style ``callback(err, result)`` function,
|
||||
you can pass the result of the promise into it with something like:
|
||||
|
||||
```javascript
|
||||
matrixClient.someMethod(arg1, arg2, function(result) {
|
||||
...
|
||||
});
|
||||
matrixClient.someMethod(arg1, arg2).nodeify(callback);
|
||||
```
|
||||
|
||||
Methods which support this will be clearly marked as returning
|
||||
``Promises``.
|
||||
|
||||
The main thing to note is that it is problematic to discard the result of a
|
||||
promise-returning function, as that will cause exceptions to go unobserved.
|
||||
|
||||
Methods which return a promise show this in their documentation.
|
||||
|
||||
Many methods in the SDK support *both* Node.js-style callbacks *and* Promises,
|
||||
via an optional ``callback`` argument. The callback support is now deprecated:
|
||||
new methods do not include a ``callback`` argument, and in the future it may be
|
||||
removed from existing methods.
|
||||
|
||||
Examples
|
||||
--------
|
||||
@@ -127,10 +206,10 @@ This section provides some useful code snippets which demonstrate the
|
||||
core functionality of the SDK. These examples assume the SDK is setup like this:
|
||||
|
||||
```javascript
|
||||
var sdk = require("matrix-js-sdk");
|
||||
var myUserId = "@example:localhost";
|
||||
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
|
||||
var matrixClient = sdk.createClient({
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const myUserId = "@example:localhost";
|
||||
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
|
||||
const matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId
|
||||
@@ -142,15 +221,15 @@ core functionality of the SDK. These examples assume the SDK is setup like this:
|
||||
```javascript
|
||||
matrixClient.on("RoomMember.membership", function(event, member) {
|
||||
if (member.membership === "invite" && member.userId === myUserId) {
|
||||
matrixClient.joinRoom(member.roomId).done(function() {
|
||||
matrixClient.joinRoom(member.roomId).then(function() {
|
||||
console.log("Auto-joined %s", member.roomId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
|
||||
### Print out messages for all rooms
|
||||
|
||||
```javascript
|
||||
@@ -166,7 +245,7 @@ core functionality of the SDK. These examples assume the SDK is setup like this:
|
||||
"(%s) %s :: %s", room.name, event.getSender(), event.getContent().body
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
@@ -183,11 +262,11 @@ Output:
|
||||
|
||||
```javascript
|
||||
matrixClient.on("RoomState.members", function(event, state, member) {
|
||||
var room = matrixClient.getRoom(state.roomId);
|
||||
const room = matrixClient.getRoom(state.roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
var memberList = state.getMembers();
|
||||
const memberList = state.getMembers();
|
||||
console.log(room.name);
|
||||
console.log(Array(room.name.length + 1).join("=")); // underline
|
||||
for (var i = 0; i < memberList.length; i++) {
|
||||
@@ -198,10 +277,10 @@ Output:
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
matrixClient.startClient();
|
||||
```
|
||||
|
||||
|
||||
Output:
|
||||
```
|
||||
My Room
|
||||
@@ -211,7 +290,7 @@ Output:
|
||||
(join) Bob
|
||||
(invite) @charlie:localhost
|
||||
```
|
||||
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
@@ -222,13 +301,53 @@ This SDK uses JSDoc3 style comments. You can manually build and
|
||||
host the API reference from the source files like this:
|
||||
|
||||
```
|
||||
$ npm run gendoc
|
||||
$ yarn gendoc
|
||||
$ cd .jsdoc
|
||||
$ python -m SimpleHTTPServer 8005
|
||||
```
|
||||
|
||||
|
||||
Then visit ``http://localhost:8005`` to see the API docs.
|
||||
|
||||
End-to-end encryption support
|
||||
=============================
|
||||
|
||||
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||
application to make libolm available, via the ``Olm`` global.
|
||||
|
||||
It is also necessary to call ``await matrixClient.initCrypto()`` after creating a new
|
||||
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
|
||||
initialise the crypto layer.
|
||||
|
||||
If the ``Olm`` global is not available, the SDK will show a warning, as shown
|
||||
below; ``initCrypto()`` will also fail.
|
||||
|
||||
```
|
||||
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
|
||||
```
|
||||
|
||||
If the crypto layer is not (successfully) initialised, the SDK will continue to
|
||||
work for unencrypted rooms, but it will not support the E2E parts of the Matrix
|
||||
specification.
|
||||
|
||||
To provide the Olm library in a browser application:
|
||||
|
||||
* download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
|
||||
* load ``olm.js`` as a ``<script>`` *before* ``browser-matrix.js``.
|
||||
|
||||
To provide the Olm library in a node.js application:
|
||||
|
||||
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``
|
||||
(replace the URL with the latest version you want to use from
|
||||
https://packages.matrix.org/npm/olm/)
|
||||
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
|
||||
|
||||
If you want to package Olm as dependency for your node.js application, you can
|
||||
use ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``. If your
|
||||
application also works without e2e crypto enabled, add ``--optional`` to mark it
|
||||
as an optional dependency.
|
||||
|
||||
|
||||
Contributing
|
||||
============
|
||||
*This section is for people who want to modify the SDK. If you just
|
||||
@@ -236,7 +355,7 @@ want to use this SDK, skip this section.*
|
||||
|
||||
First, you need to pull in the right build tools:
|
||||
```
|
||||
$ npm install
|
||||
$ yarn install
|
||||
```
|
||||
|
||||
Building
|
||||
@@ -244,20 +363,15 @@ Building
|
||||
|
||||
To build a browser version from scratch when developing::
|
||||
```
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
To constantly do builds when files are modified (using ``watchify``)::
|
||||
```
|
||||
$ npm run watch
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
To run tests (Jasmine)::
|
||||
```
|
||||
$ npm test
|
||||
$ yarn test
|
||||
```
|
||||
|
||||
|
||||
To run linting:
|
||||
```
|
||||
$ npm run lint
|
||||
$ yarn lint
|
||||
```
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
There is a script `release.sh` which does the following, but if you need to do
|
||||
a release manually, here are the steps:
|
||||
|
||||
- `git checkout -b release-v0.x.x`
|
||||
- Update `CHANGELOG.md`
|
||||
- `npm version 0.x.x`
|
||||
- Merge `release-v0.x.x` onto `master`.
|
||||
- Push `master`.
|
||||
- Push the tag: `git push --tags`
|
||||
- `npm publish`
|
||||
- Generate documentation: `npm run gendoc` (this outputs HTML to `.jsdoc`)
|
||||
- Copy the documentation from `.jsdoc` to the `gh-pages` branch and update `index.html`
|
||||
- Merge `master` onto `develop`.
|
||||
- Push `develop`.
|
||||
@@ -1,4 +0,0 @@
|
||||
var matrixcs = require("./lib/matrix");
|
||||
matrixcs.request(require("browser-request"));
|
||||
module.exports = matrixcs; // keep export for browserify package deps
|
||||
global.matrixcs = matrixcs;
|
||||
Vendored
-5826
File diff suppressed because it is too large
Load Diff
-3
File diff suppressed because one or more lines are too long
Vendored
-6490
File diff suppressed because it is too large
Load Diff
-2
File diff suppressed because one or more lines are too long
Vendored
-9900
File diff suppressed because it is too large
Load Diff
-5
File diff suppressed because one or more lines are too long
Vendored
-10023
File diff suppressed because it is too large
Load Diff
-3
File diff suppressed because one or more lines are too long
Vendored
-10864
File diff suppressed because it is too large
Load Diff
-5
File diff suppressed because one or more lines are too long
Vendored
-15420
File diff suppressed because it is too large
Load Diff
-5
File diff suppressed because one or more lines are too long
Vendored
-15577
File diff suppressed because it is too large
Load Diff
-7
File diff suppressed because one or more lines are too long
Vendored
-15785
File diff suppressed because it is too large
Load Diff
-6
File diff suppressed because one or more lines are too long
Vendored
-15832
File diff suppressed because it is too large
Load Diff
-7
File diff suppressed because one or more lines are too long
Vendored
-16305
File diff suppressed because it is too large
Load Diff
-8
File diff suppressed because one or more lines are too long
Vendored
-16327
File diff suppressed because it is too large
Load Diff
-8
File diff suppressed because one or more lines are too long
Vendored
-1
@@ -1 +0,0 @@
|
||||
Release builds and development builds will reside here.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Browser Storage Notes
|
||||
|
||||
## Overview
|
||||
|
||||
Browsers examined: Firefox 67, Chrome 75
|
||||
|
||||
The examination below applies to the default, non-persistent storage policy.
|
||||
|
||||
## Quota Measurement
|
||||
|
||||
Browsers appear to enforce and measure the quota in terms of space on disk, not
|
||||
data stored, so you may be able to store more data than the simple sum of all
|
||||
input data depending on how compressible your data is.
|
||||
|
||||
## Quota Limit
|
||||
|
||||
Specs and documentation suggest we should consistently receive
|
||||
`QuotaExceededError` when we're near space limits, but the reality is a bit
|
||||
blurrier.
|
||||
|
||||
When we are low on disk space overall or near the group limit / origin quota:
|
||||
|
||||
* Chrome
|
||||
* Log database may fail to start with AbortError
|
||||
* IndexedDB fails to start for crypto: AbortError in connect from
|
||||
indexeddb-store-worker
|
||||
* When near the quota, QuotaExceededError is used more consistently
|
||||
* Firefox
|
||||
* The first error will be QuotaExceededError
|
||||
* Future write attempts will fail with various errors when space is low,
|
||||
including nonsense like "InvalidStateError: A mutation operation was
|
||||
attempted on a database that did not allow mutations."
|
||||
* Once you start getting errors, the DB is effectively wedged in read-only
|
||||
mode
|
||||
* Can revive access if you reopen the DB
|
||||
|
||||
## Cache Eviction
|
||||
|
||||
While the Storage Standard says all storage for an origin group should be
|
||||
limited by a single quota, in practice, browsers appear to handle `localStorage`
|
||||
separately from the others, so it has a separate quota limit and isn't evicted
|
||||
when low on space.
|
||||
|
||||
* Chrome, Firefox
|
||||
* IndexedDB for origin deleted
|
||||
* Local Storage remains in place
|
||||
|
||||
## Persistent Storage
|
||||
|
||||
Storage Standard offers a `navigator.storage.persist` API that can be used to
|
||||
request persistent storage that won't be deleted by the browser because of low
|
||||
space.
|
||||
|
||||
* Chrome
|
||||
* Chrome 75 seems to grant this without any prompt based on [interaction
|
||||
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
|
||||
* Firefox
|
||||
* Firefox 67 shows a prompt to grant
|
||||
* Reverting persistent seems to require revoking permission _and_ clearing
|
||||
site data
|
||||
|
||||
## Storage Estimation
|
||||
|
||||
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
|
||||
how much space remains.
|
||||
|
||||
* Chrome, Firefox
|
||||
* Can run this at any time to request an estimate of space remaining
|
||||
* Firefox
|
||||
* Returns `0` for `usage` if a site is persisted
|
||||
@@ -0,0 +1,31 @@
|
||||
Random notes from Matthew on the two possible approaches for warning users about unexpected
|
||||
unverified devices popping up in their rooms....
|
||||
|
||||
Original idea...
|
||||
================
|
||||
|
||||
Warn when an existing user adds an unknown device to a room.
|
||||
|
||||
Warn when a user joins the room with unverified or unknown devices.
|
||||
|
||||
Warn when you initial sync if the room has any unverified devices in it.
|
||||
^ this is good enough if we're doing local storage.
|
||||
OR, better:
|
||||
Warn when you initial sync if the room has any new undefined devices since you were last there.
|
||||
=> This means persisting the rooms that devices are in, across initial syncs.
|
||||
|
||||
|
||||
Updated idea...
|
||||
===============
|
||||
|
||||
Warn when the user tries to send a message:
|
||||
- If the room has unverified devices which the user has not yet been told about in the context of this room
|
||||
...or in the context of this user? currently all verification is per-user, not per-room.
|
||||
...this should be good enough.
|
||||
|
||||
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
|
||||
throw an error when trying to encrypt if there are pure unverified devices there
|
||||
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
|
||||
- or megolm could warn which devices are causing the problems.
|
||||
|
||||
Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources?
|
||||
@@ -1,7 +1,6 @@
|
||||
"use strict";
|
||||
console.log("Loading browser sdk");
|
||||
|
||||
var client = matrixcs.createClient("http://matrix.org");
|
||||
var client = matrixcs.createClient("https://matrix.org");
|
||||
client.publicRooms(function (err, data) {
|
||||
if (err) {
|
||||
console.error("err %s", JSON.stringify(err));
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Test</title>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="data:,">
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -1 +1 @@
|
||||
../../../dist/browser-matrix-dev.js
|
||||
../../../dist/browser-matrix.js
|
||||
@@ -0,0 +1,2 @@
|
||||
olm.js
|
||||
olm.wasm
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../../dist/browser-matrix.js
|
||||
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Test Crypto in Browser</title>
|
||||
<script src="lib/olm.js"></script>
|
||||
<script src="lib/matrix.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Testing export/import of Olm devices in the browser</h1>
|
||||
<ul>
|
||||
<li>
|
||||
Make sure you built the current version of the Matrix JS SDK
|
||||
(<code>yarn build</code>)
|
||||
</li>
|
||||
<li>
|
||||
copy <code>olm.js</code> and <code>olm.wasm</code>
|
||||
from a recent release of Olm (was tested with version 3.1.4)
|
||||
in directory <code>lib/</code>
|
||||
</li>
|
||||
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
|
||||
<li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
|
||||
<li>
|
||||
in the JS console, do:
|
||||
<pre>
|
||||
aliceMatrixClient = await newMatrixClient("alice-"+randomHex());
|
||||
await aliceMatrixClient.exportDevice();
|
||||
await aliceMatrixClient.getAccessToken();
|
||||
</pre>
|
||||
</li>
|
||||
<li>
|
||||
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere
|
||||
(<strong>not</strong> in a JS variable as it will be destroyed when you refresh the page)
|
||||
</li>
|
||||
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
|
||||
<li>
|
||||
Do the following, replacing <code>ALICE_ID</code>
|
||||
with the user ID of Alice (you can find it in the exported data)
|
||||
<pre>
|
||||
bobMatrixClient = await newMatrixClient("bob-"+randomHex());
|
||||
roomId = await bobMatrixClient.createEncryptedRoom([ALICE_ID]);
|
||||
await bobMatrixClient.sendTextMessage('Hi Alice!', roomId);
|
||||
</pre>
|
||||
</li>
|
||||
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
|
||||
<li>
|
||||
Now do the following, using the exported data and the access token you saved previously:
|
||||
<pre>
|
||||
aliceMatrixClient = await importMatrixClient(EXPORTED_DATA, ACCESS_TOKEN);
|
||||
</pre>
|
||||
</li>
|
||||
<li>You should see the message sent by Bob printed in the console.</li>
|
||||
</ul>
|
||||
|
||||
<script src="olm-device-export-import.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,122 @@
|
||||
if (!Olm) {
|
||||
console.error(
|
||||
"global.Olm does not seem to be present."
|
||||
+ " Did you forget to add olm in the lib/ directory?"
|
||||
);
|
||||
}
|
||||
|
||||
const BASE_URL = 'http://localhost:8008';
|
||||
const ROOM_CRYPTO_CONFIG = { algorithm: 'm.megolm.v1.aes-sha2' };
|
||||
const PASSWORD = 'password';
|
||||
|
||||
// useful to create new usernames
|
||||
window.randomHex = () => Math.floor(Math.random() * (10**6)).toString(16);
|
||||
|
||||
window.newMatrixClient = async function (username) {
|
||||
const registrationClient = matrixcs.createClient(BASE_URL);
|
||||
|
||||
const userRegisterResult = await registrationClient.register(
|
||||
username,
|
||||
PASSWORD,
|
||||
null,
|
||||
{ type: 'm.login.dummy' }
|
||||
);
|
||||
|
||||
const matrixClient = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
userId: userRegisterResult.user_id,
|
||||
accessToken: userRegisterResult.access_token,
|
||||
deviceId: userRegisterResult.device_id,
|
||||
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
|
||||
cryptoStore: new matrixcs.MemoryCryptoStore(),
|
||||
});
|
||||
|
||||
extendMatrixClient(matrixClient);
|
||||
|
||||
await matrixClient.initCrypto();
|
||||
await matrixClient.startClient();
|
||||
return matrixClient;
|
||||
}
|
||||
|
||||
window.importMatrixClient = async function (exportedDevice, accessToken) {
|
||||
const matrixClient = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
deviceToImport: exportedDevice,
|
||||
accessToken,
|
||||
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
|
||||
cryptoStore: new matrixcs.MemoryCryptoStore(),
|
||||
});
|
||||
|
||||
extendMatrixClient(matrixClient);
|
||||
|
||||
await matrixClient.initCrypto();
|
||||
await matrixClient.startClient();
|
||||
return matrixClient;
|
||||
}
|
||||
|
||||
function extendMatrixClient(matrixClient) {
|
||||
// automatic join
|
||||
matrixClient.on('RoomMember.membership', async (event, member) => {
|
||||
if (member.membership === 'invite' && member.userId === matrixClient.getUserId()) {
|
||||
await matrixClient.joinRoom(member.roomId);
|
||||
// setting up of room encryption seems to be triggered automatically
|
||||
// but if we don't wait for it the first messages we send are unencrypted
|
||||
await matrixClient.setRoomEncryption(member.roomId, { algorithm: 'm.megolm.v1.aes-sha2' })
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.onDecryptedMessage = message => {
|
||||
console.log('Got encrypted message: ', message);
|
||||
}
|
||||
|
||||
matrixClient.on('Event.decrypted', (event) => {
|
||||
if (event.getType() === 'm.room.message'){
|
||||
matrixClient.onDecryptedMessage(event.getContent().body);
|
||||
} else {
|
||||
console.log('decrypted an event of type', event.getType());
|
||||
console.log(event);
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.createEncryptedRoom = async function(usersToInvite) {
|
||||
const {
|
||||
room_id: roomId,
|
||||
} = await this.createRoom({
|
||||
visibility: 'private',
|
||||
invite: usersToInvite,
|
||||
});
|
||||
|
||||
// matrixClient.setRoomEncryption() only updates local state
|
||||
// but does not send anything to the server
|
||||
// (see https://github.com/matrix-org/matrix-js-sdk/issues/905)
|
||||
// so we do it ourselves with 'sendStateEvent'
|
||||
await this.sendStateEvent(
|
||||
roomId, 'm.room.encryption', ROOM_CRYPTO_CONFIG,
|
||||
);
|
||||
await this.setRoomEncryption(
|
||||
roomId, ROOM_CRYPTO_CONFIG,
|
||||
);
|
||||
|
||||
// Marking all devices as verified
|
||||
let room = this.getRoom(roomId);
|
||||
let members = (await room.getEncryptionTargetMembers()).map(x => x["userId"])
|
||||
let memberkeys = await this.downloadKeys(members);
|
||||
for (const userId in memberkeys) {
|
||||
for (const deviceId in memberkeys[userId]) {
|
||||
await this.setDeviceVerified(userId, deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
return roomId;
|
||||
}
|
||||
|
||||
matrixClient.sendTextMessage = async function(message, roomId) {
|
||||
return matrixClient.sendMessage(
|
||||
roomId,
|
||||
{
|
||||
body: message,
|
||||
msgtype: 'm.text',
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
+28
-25
@@ -1,5 +1,3 @@
|
||||
"use strict";
|
||||
|
||||
var myUserId = "@example:localhost";
|
||||
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
|
||||
var sdk = require("matrix-js-sdk");
|
||||
@@ -56,7 +54,7 @@ rl.on('line', function(line) {
|
||||
}
|
||||
}
|
||||
if (notSentEvent) {
|
||||
matrixClient.resendEvent(notSentEvent, viewingRoom).done(function() {
|
||||
matrixClient.resendEvent(notSentEvent, viewingRoom).then(function() {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
@@ -70,7 +68,7 @@ rl.on('line', function(line) {
|
||||
}
|
||||
else if (line.indexOf("/more ") === 0) {
|
||||
var amount = parseInt(line.split(" ")[1]) || 20;
|
||||
matrixClient.scrollback(viewingRoom, amount).done(function(room) {
|
||||
matrixClient.scrollback(viewingRoom, amount).then(function(room) {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
@@ -79,7 +77,7 @@ rl.on('line', function(line) {
|
||||
}
|
||||
else if (line.indexOf("/invite ") === 0) {
|
||||
var userId = line.split(" ")[1].trim();
|
||||
matrixClient.invite(viewingRoom.roomId, userId).done(function() {
|
||||
matrixClient.invite(viewingRoom.roomId, userId).then(function() {
|
||||
printMessages();
|
||||
rl.prompt();
|
||||
}, function(err) {
|
||||
@@ -92,7 +90,7 @@ rl.on('line', function(line) {
|
||||
matrixClient.uploadContent({
|
||||
stream: stream,
|
||||
name: filename
|
||||
}).done(function(url) {
|
||||
}).then(function(url) {
|
||||
var content = {
|
||||
msgtype: "m.file",
|
||||
body: filename,
|
||||
@@ -116,7 +114,7 @@ rl.on('line', function(line) {
|
||||
viewingRoom = roomList[roomIndex];
|
||||
if (viewingRoom.getMember(myUserId).membership === "invite") {
|
||||
// join the room first
|
||||
matrixClient.joinRoom(viewingRoom.roomId).done(function(room) {
|
||||
matrixClient.joinRoom(viewingRoom.roomId).then(function(room) {
|
||||
setRoomList();
|
||||
viewingRoom = room;
|
||||
printMessages();
|
||||
@@ -128,18 +126,22 @@ rl.on('line', function(line) {
|
||||
else {
|
||||
printMessages();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rl.prompt();
|
||||
});
|
||||
// ==== END User input
|
||||
|
||||
// show the room list after syncing.
|
||||
matrixClient.on("syncComplete", function() {
|
||||
setRoomList();
|
||||
printRoomList();
|
||||
printHelp();
|
||||
rl.prompt();
|
||||
matrixClient.on("sync", function(state, prevState, data) {
|
||||
switch (state) {
|
||||
case "PREPARED":
|
||||
setRoomList();
|
||||
printRoomList();
|
||||
printHelp();
|
||||
rl.prompt();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.on("Room", function() {
|
||||
@@ -198,9 +200,9 @@ function printRoomList() {
|
||||
dateStr = new Date(msg.getTs()).toISOString().replace(
|
||||
/T/, ' ').replace(/\..+/, '');
|
||||
}
|
||||
var me = roomList[i].getMember(myUserId);
|
||||
if (me) {
|
||||
fmt = fmts[me.membership];
|
||||
var myMembership = roomList[i].getMyMembership();
|
||||
if (myMembership) {
|
||||
fmt = fmts[myMembership];
|
||||
}
|
||||
var roomName = fixWidth(roomList[i].name, 25);
|
||||
print(
|
||||
@@ -277,8 +279,8 @@ function printMemberList(room) {
|
||||
member.membership + new Array(10 - member.membership.length).join(" ")
|
||||
);
|
||||
print(
|
||||
"%s"+fmt(" :: ")+"%s"+fmt(" (")+"%s"+fmt(")"),
|
||||
membershipWithPadding, member.name,
|
||||
"%s"+fmt(" :: ")+"%s"+fmt(" (")+"%s"+fmt(")"),
|
||||
membershipWithPadding, member.name,
|
||||
(member.userId === myUserId ? "Me" : member.userId),
|
||||
fmt
|
||||
);
|
||||
@@ -286,26 +288,27 @@ function printMemberList(room) {
|
||||
}
|
||||
|
||||
function printRoomInfo(room) {
|
||||
var eventDict = room.currentState.events;
|
||||
var eventMap = room.currentState.events;
|
||||
var eTypeHeader = " Event Type(state_key) ";
|
||||
var sendHeader = " Sender ";
|
||||
// pad content to 100
|
||||
var restCount = (
|
||||
100 - "Content".length - " | ".length - " | ".length -
|
||||
100 - "Content".length - " | ".length - " | ".length -
|
||||
eTypeHeader.length - sendHeader.length
|
||||
);
|
||||
var padSide = new Array(Math.floor(restCount/2)).join(" ");
|
||||
var contentHeader = padSide + "Content" + padSide;
|
||||
print(eTypeHeader+sendHeader+contentHeader);
|
||||
print(new Array(100).join("-"));
|
||||
Object.keys(eventDict).forEach(function(eventType) {
|
||||
eventMap.keys().forEach(function(eventType) {
|
||||
if (eventType === "m.room.member") { return; } // use /members instead.
|
||||
Object.keys(eventDict[eventType]).forEach(function(stateKey) {
|
||||
var eventEventMap = eventMap.get(eventType);
|
||||
eventEventMap.keys().forEach(function(stateKey) {
|
||||
var typeAndKey = eventType + (
|
||||
stateKey.length > 0 ? "("+stateKey+")" : ""
|
||||
);
|
||||
var typeStr = fixWidth(typeAndKey, eTypeHeader.length);
|
||||
var event = eventDict[eventType][stateKey];
|
||||
var event = eventEventMap.get(stateKey);
|
||||
var sendStr = fixWidth(event.getSender(), sendHeader.length);
|
||||
var contentStr = fixWidth(
|
||||
JSON.stringify(event.getContent()), contentHeader.length
|
||||
@@ -338,7 +341,7 @@ function printLine(event) {
|
||||
|
||||
var maxNameWidth = 15;
|
||||
if (name.length > maxNameWidth) {
|
||||
name = name.substr(0, maxNameWidth-1) + "\u2026";
|
||||
name = name.slice(0, maxNameWidth-1) + "\u2026";
|
||||
}
|
||||
|
||||
if (event.getType() === "m.room.message") {
|
||||
@@ -395,7 +398,7 @@ function print(str, formatter) {
|
||||
|
||||
function fixWidth(str, len) {
|
||||
if (str.length > len) {
|
||||
return str.substr(0, len-2) + "\u2026";
|
||||
return str.substring(0, len-2) + "\u2026";
|
||||
}
|
||||
else if (str.length < len) {
|
||||
return str + new Array(len - str.length).join(" ");
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"use strict";
|
||||
console.log("Loading browser sdk");
|
||||
var BASE_URL = "https://matrix.org";
|
||||
var TOKEN = "accesstokengoeshere";
|
||||
var USER_ID = "@username:localhost";
|
||||
var ROOM_ID = "!room:id";
|
||||
const BASE_URL = "https://matrix.org";
|
||||
const TOKEN = "accesstokengoeshere";
|
||||
const USER_ID = "@username:localhost";
|
||||
const ROOM_ID = "!room:id";
|
||||
const DEVICE_ID = "some_device_id";
|
||||
|
||||
|
||||
var client = matrixcs.createClient({
|
||||
const client = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
accessToken: TOKEN,
|
||||
userId: USER_ID
|
||||
userId: USER_ID,
|
||||
deviceId: DEVICE_ID
|
||||
});
|
||||
var call;
|
||||
let call;
|
||||
|
||||
function disableButtons(place, answer, hangup) {
|
||||
document.getElementById("hangup").disabled = hangup;
|
||||
@@ -20,7 +20,7 @@ function disableButtons(place, answer, hangup) {
|
||||
}
|
||||
|
||||
function addListeners(call) {
|
||||
var lastError = "";
|
||||
let lastError = "";
|
||||
call.on("hangup", function() {
|
||||
disableButtons(false, true, true);
|
||||
document.getElementById("result").innerHTML = (
|
||||
@@ -32,6 +32,23 @@ function addListeners(call) {
|
||||
call.hangup();
|
||||
disableButtons(false, true, true);
|
||||
});
|
||||
call.on("feeds_changed", function(feeds) {
|
||||
const localFeed = feeds.find((feed) => feed.isLocal());
|
||||
const remoteFeed = feeds.find((feed) => !feed.isLocal());
|
||||
|
||||
const remoteElement = document.getElementById("remote");
|
||||
const localElement = document.getElementById("local");
|
||||
|
||||
if (remoteFeed) {
|
||||
remoteElement.srcObject = remoteFeed.stream;
|
||||
remoteElement.play();
|
||||
}
|
||||
if (localFeed) {
|
||||
localElement.muted = true;
|
||||
localElement.srcObject = localFeed.stream;
|
||||
localElement.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
@@ -44,7 +61,15 @@ window.onload = function() {
|
||||
disableButtons(true, true, true);
|
||||
};
|
||||
|
||||
client.on("syncComplete", function () {
|
||||
client.on("sync", function(state, prevState, data) {
|
||||
switch (state) {
|
||||
case "PREPARED":
|
||||
syncComplete();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function syncComplete() {
|
||||
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
|
||||
disableButtons(false, true, true);
|
||||
|
||||
@@ -55,10 +80,7 @@ client.on("syncComplete", function () {
|
||||
);
|
||||
console.log("Call => %s", call);
|
||||
addListeners(call);
|
||||
call.placeVideoCall(
|
||||
document.getElementById("remote"),
|
||||
document.getElementById("local")
|
||||
);
|
||||
call.placeVideoCall();
|
||||
document.getElementById("result").innerHTML = "<p>Placed call.</p>";
|
||||
disableButtons(true, true, false);
|
||||
};
|
||||
@@ -85,5 +107,5 @@ client.on("syncComplete", function () {
|
||||
call = c;
|
||||
addListeners(call);
|
||||
});
|
||||
});
|
||||
}
|
||||
client.startClient();
|
||||
|
||||
+21
-13
@@ -1,26 +1,34 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>VoIP Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
<title>VoIP Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
You can place and receive calls with this example. Make sure to edit the
|
||||
You can place and receive calls with this example. Make sure to edit the
|
||||
constants in <code>browserTest.js</code> first.
|
||||
<div id="config"></div>
|
||||
<div id="result"></div>
|
||||
<button id="call">Place Call</button>
|
||||
<button id="answer">Answer Call</button>
|
||||
<button id="hangup">Hangup Call</button>
|
||||
<div id="videoBackground">
|
||||
<div id="videoContainer">
|
||||
<video id="remote"></video>
|
||||
</div>
|
||||
</div>
|
||||
<div id="videoBackground">
|
||||
<div id="videoContainer">
|
||||
<video id="local"></video>
|
||||
</div>
|
||||
<div id="videoBackground" class="video-background">
|
||||
<video class="video-element" id="local"></video>
|
||||
<video class="video-element" id="remote"></video>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<style>
|
||||
.video-background {
|
||||
height: 500px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.video-element {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1 +1 @@
|
||||
../../../dist/browser-matrix-dev.js
|
||||
../../../dist/browser-matrix.js
|
||||
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
|
||||
|
||||
# now run our checks
|
||||
cd "$tmpdir"
|
||||
npm run lint
|
||||
yarn lint
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
var matrixcs = require("./lib/matrix");
|
||||
matrixcs.request(require("request"));
|
||||
module.exports = matrixcs;
|
||||
|
||||
var utils = require("./lib/utils");
|
||||
utils.runPolyfills();
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
#!/bin/bash -l
|
||||
export NVM_DIR="/home/jenkins/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
||||
nvm use 0.10
|
||||
npm install
|
||||
npm test
|
||||
jshint --reporter=checkstyle -c .jshint lib spec > jshint.xml || echo "jshint finished with return code $?"
|
||||
gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r lib/ -r spec/ > gjslint.log || echo "gjslint finished with return code $?"
|
||||
|
||||
# delete the old tarball, if it exists
|
||||
rm -f matrix-js-sdk-*.tgz
|
||||
|
||||
npm pack
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"tags": {
|
||||
"allowUnknownTags": true
|
||||
},
|
||||
"plugins": [
|
||||
"node_modules/better-docs/category",
|
||||
"node_modules/better-docs/typescript"
|
||||
],
|
||||
"source": {
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"includePattern": ".(ts|js)$"
|
||||
},
|
||||
"opts": {
|
||||
"encoding": "utf8",
|
||||
"destination": ".jsdoc",
|
||||
"readme": "README.md",
|
||||
"recurse": true,
|
||||
"verbose": true,
|
||||
"template": "node_modules/docdash"
|
||||
},
|
||||
"docdash": {
|
||||
"static": true,
|
||||
"private": false,
|
||||
"search": true,
|
||||
"collapse": true,
|
||||
"typedefs": true
|
||||
}
|
||||
}
|
||||
-3664
File diff suppressed because it is too large
Load Diff
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
/**
|
||||
* @module content-repo
|
||||
*/
|
||||
var utils = require("./utils");
|
||||
|
||||
/** Content Repo utility functions */
|
||||
module.exports = {
|
||||
/**
|
||||
* Get the HTTP URL for an MXC URI.
|
||||
* @param {string} baseUrl The base homeserver url which has a content repo.
|
||||
* @param {string} mxc The mxc:// URI.
|
||||
* @param {Number} width The desired width of the thumbnail.
|
||||
* @param {Number} height The desired height of the thumbnail.
|
||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||
* "crop" or "scale".
|
||||
* @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
|
||||
* directly. Fetching such URLs will leak information about the user to
|
||||
* anyone they share a room with. If false, will return the emptry string
|
||||
* for such URLs.
|
||||
* @return {string} The complete URL to the content.
|
||||
*/
|
||||
getHttpUriForMxc: function(baseUrl, mxc, width, height,
|
||||
resizeMethod, allowDirectLinks) {
|
||||
if (typeof mxc !== "string" || !mxc) {
|
||||
return '';
|
||||
}
|
||||
if (mxc.indexOf("mxc://") !== 0) {
|
||||
if (allowDirectLinks) {
|
||||
return mxc;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
var serverAndMediaId = mxc.slice(6); // strips mxc://
|
||||
var prefix = "/_matrix/media/v1/download/";
|
||||
var params = {};
|
||||
|
||||
if (width) {
|
||||
params.width = width;
|
||||
}
|
||||
if (height) {
|
||||
params.height = height;
|
||||
}
|
||||
if (resizeMethod) {
|
||||
params.method = resizeMethod;
|
||||
}
|
||||
if (utils.keys(params).length > 0) {
|
||||
// these are thumbnailing params so they probably want the
|
||||
// thumbnailing API...
|
||||
prefix = "/_matrix/media/v1/thumbnail/";
|
||||
}
|
||||
|
||||
var fragmentOffset = serverAndMediaId.indexOf("#"),
|
||||
fragment = "";
|
||||
if (fragmentOffset >= 0) {
|
||||
fragment = serverAndMediaId.substr(fragmentOffset);
|
||||
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
|
||||
}
|
||||
return baseUrl + prefix + serverAndMediaId +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params))) + fragment;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an identicon URL from an arbitrary string.
|
||||
* @param {string} baseUrl The base homeserver url which has a content repo.
|
||||
* @param {string} identiconString The string to create an identicon for.
|
||||
* @param {Number} width The desired width of the image in pixels. Default: 96.
|
||||
* @param {Number} height The desired height of the image in pixels. Default: 96.
|
||||
* @return {string} The complete URL to the identicon.
|
||||
*/
|
||||
getIdenticonUri: function(baseUrl, identiconString, width, height) {
|
||||
if (!identiconString) {
|
||||
return null;
|
||||
}
|
||||
if (!width) { width = 96; }
|
||||
if (!height) { height = 96; }
|
||||
var params = {
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
|
||||
var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
|
||||
$ident: identiconString
|
||||
});
|
||||
return baseUrl + path +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params)));
|
||||
}
|
||||
};
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
/**
|
||||
* @module filter
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
* @param {string} keyNesting
|
||||
* @param {*} val
|
||||
*/
|
||||
function setProp(obj, keyNesting, val) {
|
||||
var nestedKeys = keyNesting.split(".");
|
||||
var currentObj = obj;
|
||||
for (var i = 0; i < (nestedKeys.length - 1); i++) {
|
||||
if (!currentObj[nestedKeys[i]]) {
|
||||
currentObj[nestedKeys[i]] = {};
|
||||
}
|
||||
currentObj = currentObj[nestedKeys[i]];
|
||||
}
|
||||
currentObj[nestedKeys[nestedKeys.length - 1]] = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new Filter.
|
||||
* @constructor
|
||||
* @param {string} userId The user ID for this filter.
|
||||
* @param {string=} filterId The filter ID if known.
|
||||
* @prop {string} userId The user ID of the filter
|
||||
* @prop {?string} filterId The filter ID
|
||||
*/
|
||||
function Filter(userId, filterId) {
|
||||
this.userId = userId;
|
||||
this.filterId = filterId;
|
||||
this.definition = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON body of the filter.
|
||||
* @return {Object} The filter definition
|
||||
*/
|
||||
Filter.prototype.getDefinition = function() {
|
||||
return this.definition;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the JSON body of the filter
|
||||
* @param {Object} definition The filter definition
|
||||
*/
|
||||
Filter.prototype.setDefinition = function(definition) {
|
||||
this.definition = definition;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the max number of events to return for each room's timeline.
|
||||
* @param {Number} limit The max number of events to return for each room.
|
||||
*/
|
||||
Filter.prototype.setTimelineLimit = function(limit) {
|
||||
setProp(this.definition, "room.timeline.limit", limit);
|
||||
};
|
||||
|
||||
/**
|
||||
* Control whether left rooms should be included in responses.
|
||||
* @param {boolean} includeLeave True to make rooms the user has left appear
|
||||
* in responses.
|
||||
*/
|
||||
Filter.prototype.setIncludeLeaveRooms = function(includeLeave) {
|
||||
setProp(this.definition, "room.include_leave", includeLeave);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a filter from existing data.
|
||||
* @static
|
||||
* @param {string} userId
|
||||
* @param {string} filterId
|
||||
* @param {Object} jsonObj
|
||||
* @return {Filter}
|
||||
*/
|
||||
Filter.fromJson = function(userId, filterId, jsonObj) {
|
||||
var filter = new Filter(userId, filterId);
|
||||
filter.setDefinition(jsonObj);
|
||||
return filter;
|
||||
};
|
||||
|
||||
/** The Filter class */
|
||||
module.exports = Filter;
|
||||
-573
@@ -1,573 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
/**
|
||||
* This is an internal module. See {@link MatrixHttpApi} for the public class.
|
||||
* @module http-api
|
||||
*/
|
||||
var q = require("q");
|
||||
var utils = require("./utils");
|
||||
|
||||
// we use our own implementation of setTimeout, so that if we get suspended in
|
||||
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
|
||||
// waiting for the delay to elapse.
|
||||
var callbacks = require("./realtime-callbacks");
|
||||
|
||||
/*
|
||||
TODO:
|
||||
- CS: complete register function (doing stages)
|
||||
- Identity server: linkEmail, authEmail, bindEmail, lookup3pid
|
||||
*/
|
||||
|
||||
/**
|
||||
* A constant representing the URI path for release 0 of the Client-Server HTTP API.
|
||||
*/
|
||||
module.exports.PREFIX_R0 = "/_matrix/client/r0";
|
||||
|
||||
/**
|
||||
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
|
||||
*/
|
||||
module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
|
||||
|
||||
/**
|
||||
* URI path for the identity API
|
||||
*/
|
||||
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
|
||||
|
||||
/**
|
||||
* URI path for the media repo API
|
||||
*/
|
||||
module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
|
||||
|
||||
/**
|
||||
* Construct a MatrixHttpApi.
|
||||
* @constructor
|
||||
* @param {EventEmitter} event_emitter The event emitter to use for emitting events
|
||||
* @param {Object} opts The options to use for this HTTP API.
|
||||
* @param {string} opts.baseUrl Required. The base client-server URL e.g.
|
||||
* 'http://localhost:8008'.
|
||||
* @param {Function} opts.request Required. The function to call for HTTP
|
||||
* requests. This function must look like function(opts, callback){ ... }.
|
||||
* @param {string} opts.prefix Required. The matrix client prefix to use, e.g.
|
||||
* '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants.
|
||||
* @param {bool} opts.onlyData True to return only the 'data' component of the
|
||||
* response (e.g. the parsed HTTP body). If false, requests will return status
|
||||
* codes and headers in addition to data. Default: false.
|
||||
* @param {string} opts.accessToken The access_token to send with requests. Can be
|
||||
* null to not send an access token.
|
||||
* @param {Object} opts.extraParams Optional. Extra query parameters to send on
|
||||
* requests.
|
||||
*/
|
||||
module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
|
||||
opts.onlyData = opts.onlyData || false;
|
||||
this.event_emitter = event_emitter;
|
||||
this.opts = opts;
|
||||
this.uploads = [];
|
||||
};
|
||||
|
||||
module.exports.MatrixHttpApi.prototype = {
|
||||
|
||||
/**
|
||||
* Get the content repository url with query parameters.
|
||||
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
|
||||
* path and query parameters respectively.
|
||||
*/
|
||||
getContentUri: function() {
|
||||
var params = {
|
||||
access_token: this.opts.accessToken
|
||||
};
|
||||
return {
|
||||
base: this.opts.baseUrl,
|
||||
path: "/_matrix/media/v1/upload",
|
||||
params: params
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload content to the Home Server
|
||||
* @param {File} file A File object (in a browser) or in Node,
|
||||
an object with properties:
|
||||
name: The file's name
|
||||
stream: A read stream
|
||||
* @param {Function} callback Optional. The callback to invoke on
|
||||
* success/failure. See the promise return values for more information.
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
*/
|
||||
uploadContent: function(file, callback) {
|
||||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||||
throw Error(
|
||||
"Expected callback to be a function but got " + typeof callback
|
||||
);
|
||||
}
|
||||
var defer = q.defer();
|
||||
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
|
||||
// browser-request doesn't support File objects because it deep-copies
|
||||
// the options using JSON.parse(JSON.stringify(options)). Instead of
|
||||
// loading the whole file into memory as a string and letting
|
||||
// browser-request base64 encode and then decode it again, we just
|
||||
// use XMLHttpRequest directly.
|
||||
// (browser-request doesn't support progress either, which is also kind
|
||||
// of important here)
|
||||
|
||||
var upload = { loaded: 0, total: 0 };
|
||||
|
||||
if (global.XMLHttpRequest) {
|
||||
var xhr = new global.XMLHttpRequest();
|
||||
upload.xhr = xhr;
|
||||
var cb = requestCallback(defer, callback, this.opts.onlyData);
|
||||
|
||||
var timeout_fn = function() {
|
||||
xhr.abort();
|
||||
cb(new Error('Timeout'));
|
||||
};
|
||||
|
||||
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
switch (xhr.readyState) {
|
||||
case global.XMLHttpRequest.DONE:
|
||||
callbacks.clearTimeout(xhr.timeout_timer);
|
||||
var err;
|
||||
if (!xhr.responseText) {
|
||||
err = new Error('No response body.');
|
||||
err.http_status = xhr.status;
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var resp = JSON.parse(xhr.responseText);
|
||||
if (resp.content_uri === undefined) {
|
||||
err = Error('Bad response');
|
||||
err.http_status = xhr.status;
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
cb(undefined, xhr, resp.content_uri);
|
||||
break;
|
||||
}
|
||||
};
|
||||
xhr.upload.addEventListener("progress", function(ev) {
|
||||
callbacks.clearTimeout(xhr.timeout_timer);
|
||||
upload.loaded = ev.loaded;
|
||||
upload.total = ev.total;
|
||||
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
|
||||
defer.notify(ev);
|
||||
});
|
||||
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
|
||||
url += "&filename=" + encodeURIComponent(file.name);
|
||||
|
||||
xhr.open("POST", url);
|
||||
if (file.type) {
|
||||
xhr.setRequestHeader("Content-Type", file.type);
|
||||
} else {
|
||||
// if the file doesn't have a mime type, use a default since
|
||||
// the HS errors if we don't supply one.
|
||||
xhr.setRequestHeader("Content-Type", 'application/octet-stream');
|
||||
}
|
||||
xhr.send(file);
|
||||
} else {
|
||||
var queryParams = {
|
||||
filename: file.name,
|
||||
access_token: this.opts.accessToken
|
||||
};
|
||||
upload.request = this.opts.request({
|
||||
uri: url,
|
||||
qs: queryParams,
|
||||
method: "POST",
|
||||
headers: {"Content-Type": file.type},
|
||||
body: file.stream
|
||||
}, requestCallback(defer, callback, this.opts.onlyData));
|
||||
}
|
||||
|
||||
this.uploads.push(upload);
|
||||
|
||||
var self = this;
|
||||
upload.promise = defer.promise.finally(function() {
|
||||
var uploadsKeys = Object.keys(self.uploads);
|
||||
for (var i = 0; i < uploadsKeys.length; ++i) {
|
||||
if (self.uploads[uploadsKeys[i]].promise === defer.promise) {
|
||||
self.uploads.splice(uploadsKeys[i], 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
return upload.promise;
|
||||
},
|
||||
|
||||
cancelUpload: function(promise) {
|
||||
var uploadsKeys = Object.keys(this.uploads);
|
||||
for (var i = 0; i < uploadsKeys.length; ++i) {
|
||||
var upload = this.uploads[uploadsKeys[i]];
|
||||
if (upload.promise === promise) {
|
||||
if (upload.xhr !== undefined) {
|
||||
upload.xhr.abort();
|
||||
return true;
|
||||
} else if (upload.request !== undefined) {
|
||||
upload.request.abort();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
getCurrentUploads: function() {
|
||||
return this.uploads;
|
||||
},
|
||||
|
||||
idServerRequest: function(callback, method, path, params, prefix) {
|
||||
var fullUri = this.opts.idBaseUrl + prefix + path;
|
||||
|
||||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||||
throw Error(
|
||||
"Expected callback to be a function but got " + typeof callback
|
||||
);
|
||||
}
|
||||
|
||||
var opts = {
|
||||
uri: fullUri,
|
||||
method: method,
|
||||
withCredentials: false,
|
||||
json: false,
|
||||
_matrix_opts: this.opts
|
||||
};
|
||||
if (method == 'GET') {
|
||||
opts.qs = params;
|
||||
} else {
|
||||
opts.form = params;
|
||||
}
|
||||
|
||||
var defer = q.defer();
|
||||
this.opts.request(
|
||||
opts,
|
||||
requestCallback(defer, callback, this.opts.onlyData)
|
||||
);
|
||||
// ID server does not always take JSON, so we can't use requests' 'json'
|
||||
// option as we do with the home server, but it does return JSON, so
|
||||
// parse it manually
|
||||
return defer.promise.then(function(response) {
|
||||
return JSON.parse(response);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform an authorised request to the homeserver.
|
||||
* @param {Function} callback Optional. The callback to invoke on
|
||||
* success/failure. See the promise return values for more information.
|
||||
* @param {string} method The HTTP method e.g. "GET".
|
||||
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
|
||||
* "/createRoom".
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded).
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
authedRequest: function(callback, method, path, queryParams, data, localTimeoutMs) {
|
||||
if (!queryParams) { queryParams = {}; }
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
var self = this;
|
||||
var request_promise = this.request(
|
||||
callback, method, path, queryParams, data, localTimeoutMs
|
||||
);
|
||||
request_promise.catch(function(err) {
|
||||
if (err.errcode == 'M_UNKNOWN_TOKEN') {
|
||||
self.event_emitter.emit("Session.logged_out");
|
||||
}
|
||||
});
|
||||
// return the original promise, otherwise tests break due to it having to
|
||||
// go around the event loop one more time to process the result of the request
|
||||
return request_promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform a request to the homeserver without any credentials.
|
||||
* @param {Function} callback Optional. The callback to invoke on
|
||||
* success/failure. See the promise return values for more information.
|
||||
* @param {string} method The HTTP method e.g. "GET".
|
||||
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
|
||||
* "/createRoom".
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded).
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
request: function(callback, method, path, queryParams, data, localTimeoutMs) {
|
||||
return this.requestWithPrefix(
|
||||
callback, method, path, queryParams, data, this.opts.prefix, localTimeoutMs
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform an authorised request to the homeserver with a specific path
|
||||
* prefix which overrides the default for this call only. Useful for hitting
|
||||
* different Matrix Client-Server versions.
|
||||
* @param {Function} callback Optional. The callback to invoke on
|
||||
* success/failure. See the promise return values for more information.
|
||||
* @param {string} method The HTTP method e.g. "GET".
|
||||
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
|
||||
* "/createRoom".
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded).
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
* @param {string} prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha".
|
||||
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
|
||||
prefix, localTimeoutMs) {
|
||||
var fullUri = this.opts.baseUrl + prefix + path;
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
queryParams.access_token = this.opts.accessToken;
|
||||
return this._request(
|
||||
callback, method, fullUri, queryParams, data, localTimeoutMs
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform a request to the homeserver without any credentials but with a
|
||||
* specific path prefix which overrides the default for this call only.
|
||||
* Useful for hitting different Matrix Client-Server versions.
|
||||
* @param {Function} callback Optional. The callback to invoke on
|
||||
* success/failure. See the promise return values for more information.
|
||||
* @param {string} method The HTTP method e.g. "GET".
|
||||
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
|
||||
* "/createRoom".
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded).
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
* @param {string} prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha".
|
||||
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
requestWithPrefix: function(callback, method, path, queryParams, data, prefix,
|
||||
localTimeoutMs) {
|
||||
var fullUri = this.opts.baseUrl + prefix + path;
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
return this._request(
|
||||
callback, method, fullUri, queryParams, data, localTimeoutMs
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform a request to an arbitrary URL.
|
||||
* @param {Function} callback Optional. The callback to invoke on
|
||||
* success/failure. See the promise return values for more information.
|
||||
* @param {string} method The HTTP method e.g. "GET".
|
||||
* @param {string} uri The HTTP URI
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded).
|
||||
* @param {Object} data The HTTP JSON body.
|
||||
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
|
||||
* timing out the request. If not specified, there is no timeout.
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
* headers: {Object}, code: {Number}}</code>.
|
||||
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
|
||||
* object only.
|
||||
* @return {module:http-api.MatrixError} Rejects with an error if a problem
|
||||
* occurred. This includes network problems and Matrix-specific error JSON.
|
||||
*/
|
||||
requestOtherUrl: function(callback, method, uri, queryParams, data,
|
||||
localTimeoutMs) {
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
return this._request(
|
||||
callback, method, uri, queryParams, data, localTimeoutMs
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Form and return a homeserver request URL based on the given path
|
||||
* params and prefix.
|
||||
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
|
||||
* "/createRoom".
|
||||
* @param {Object} queryParams A dict of query params (these will NOT be
|
||||
* urlencoded).
|
||||
* @param {string} prefix The full prefix to use e.g.
|
||||
* "/_matrix/client/v2_alpha".
|
||||
* @return {string} URL
|
||||
*/
|
||||
getUrl: function(path, queryParams, prefix) {
|
||||
var queryString = "";
|
||||
if (queryParams) {
|
||||
queryString = "?" + utils.encodeParams(queryParams);
|
||||
}
|
||||
return this.opts.baseUrl + prefix + path + queryString;
|
||||
},
|
||||
|
||||
_request: function(callback, method, uri, queryParams, data, localTimeoutMs) {
|
||||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||||
throw Error(
|
||||
"Expected callback to be a function but got " + typeof callback
|
||||
);
|
||||
}
|
||||
var self = this;
|
||||
if (!queryParams) {
|
||||
queryParams = {};
|
||||
}
|
||||
if (this.opts.extraParams) {
|
||||
for (var key in this.opts.extraParams) {
|
||||
if (!this.opts.extraParams.hasOwnProperty(key)) { continue; }
|
||||
queryParams[key] = this.opts.extraParams[key];
|
||||
}
|
||||
}
|
||||
var defer = q.defer();
|
||||
|
||||
var timeoutId;
|
||||
var timedOut = false;
|
||||
var req;
|
||||
if (localTimeoutMs) {
|
||||
timeoutId = callbacks.setTimeout(function() {
|
||||
timedOut = true;
|
||||
if (req && req.abort) {
|
||||
req.abort();
|
||||
}
|
||||
defer.reject(new module.exports.MatrixError({
|
||||
error: "Locally timed out waiting for a response",
|
||||
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
|
||||
timeout: localTimeoutMs
|
||||
}));
|
||||
}, localTimeoutMs);
|
||||
}
|
||||
|
||||
var reqPromise = defer.promise;
|
||||
|
||||
try {
|
||||
req = this.opts.request(
|
||||
{
|
||||
uri: uri,
|
||||
method: method,
|
||||
withCredentials: false,
|
||||
qs: queryParams,
|
||||
body: data,
|
||||
json: true,
|
||||
timeout: localTimeoutMs,
|
||||
_matrix_opts: this.opts
|
||||
},
|
||||
function(err, response, body) {
|
||||
if (localTimeoutMs) {
|
||||
callbacks.clearTimeout(timeoutId);
|
||||
if (timedOut) {
|
||||
return; // already rejected promise
|
||||
}
|
||||
}
|
||||
var handlerFn = requestCallback(defer, callback, self.opts.onlyData);
|
||||
handlerFn(err, response, body);
|
||||
}
|
||||
);
|
||||
if (req && req.abort) {
|
||||
// FIXME: This is EVIL, but I can't think of a better way to expose
|
||||
// abort() operations on underlying HTTP requests :(
|
||||
reqPromise.abort = req.abort.bind(req);
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
defer.reject(ex);
|
||||
if (callback) {
|
||||
callback(ex);
|
||||
}
|
||||
}
|
||||
return reqPromise;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Returns a callback that can be invoked by an HTTP request on completion,
|
||||
* that will either resolve or reject the given defer as well as invoke the
|
||||
* given userDefinedCallback (if any).
|
||||
*
|
||||
* If onlyData is true, the defer/callback is invoked with the body of the
|
||||
* response, otherwise the result code.
|
||||
*/
|
||||
var requestCallback = function(defer, userDefinedCallback, onlyData) {
|
||||
userDefinedCallback = userDefinedCallback || function() {};
|
||||
|
||||
return function(err, response, body) {
|
||||
if (!err && response.statusCode >= 400) {
|
||||
err = new module.exports.MatrixError(body);
|
||||
err.httpStatus = response.statusCode;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
defer.reject(err);
|
||||
userDefinedCallback(err);
|
||||
}
|
||||
else {
|
||||
var res = {
|
||||
code: response.statusCode,
|
||||
headers: response.headers,
|
||||
data: body
|
||||
};
|
||||
defer.resolve(onlyData ? body : res);
|
||||
userDefinedCallback(null, onlyData ? body : res);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a Matrix error. This is a JavaScript Error with additional
|
||||
* information specific to the standard Matrix error response.
|
||||
* @constructor
|
||||
* @param {Object} errorJson The Matrix error JSON returned from the homeserver.
|
||||
* @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN".
|
||||
* @prop {string} name Same as MatrixError.errcode but with a default unknown string.
|
||||
* @prop {string} message The Matrix 'error' value, e.g. "Missing token."
|
||||
* @prop {Object} data The raw Matrix error JSON used to construct this object.
|
||||
* @prop {integer} httpStatus The numeric HTTP status code given
|
||||
*/
|
||||
module.exports.MatrixError = function MatrixError(errorJson) {
|
||||
errorJson = errorJson || {};
|
||||
this.errcode = errorJson.errcode;
|
||||
this.name = errorJson.errcode || "Unknown error code";
|
||||
this.message = errorJson.error || "Unknown message";
|
||||
this.data = errorJson;
|
||||
};
|
||||
module.exports.MatrixError.prototype = Object.create(Error.prototype);
|
||||
/** */
|
||||
module.exports.MatrixError.prototype.constructor = module.exports.MatrixError;
|
||||
-137
@@ -1,137 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/** The {@link module:models/event.MatrixEvent|MatrixEvent} class. */
|
||||
module.exports.MatrixEvent = require("./models/event").MatrixEvent;
|
||||
/** The {@link module:models/event.EventStatus|EventStatus} enum. */
|
||||
module.exports.EventStatus = require("./models/event").EventStatus;
|
||||
/** The {@link module:store/memory.MatrixInMemoryStore|MatrixInMemoryStore} class. */
|
||||
module.exports.MatrixInMemoryStore = require("./store/memory").MatrixInMemoryStore;
|
||||
/** The {@link module:store/webstorage~WebStorageStore|WebStorageStore} class.
|
||||
* <strong>Work in progress; unstable.</strong> */
|
||||
module.exports.WebStorageStore = require("./store/webstorage");
|
||||
/** The {@link module:http-api.MatrixHttpApi|MatrixHttpApi} class. */
|
||||
module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
|
||||
/** The {@link module:http-api.MatrixError|MatrixError} class. */
|
||||
module.exports.MatrixError = require("./http-api").MatrixError;
|
||||
/** The {@link module:client.MatrixClient|MatrixClient} class. */
|
||||
module.exports.MatrixClient = require("./client").MatrixClient;
|
||||
/** The {@link module:models/room~Room|Room} class. */
|
||||
module.exports.Room = require("./models/room");
|
||||
/** The {@link module:models/event-timeline~EventTimeline} class. */
|
||||
module.exports.EventTimeline = require("./models/event-timeline");
|
||||
/** The {@link module:models/room-member~RoomMember|RoomMember} class. */
|
||||
module.exports.RoomMember = require("./models/room-member");
|
||||
/** The {@link module:models/room-state~RoomState|RoomState} class. */
|
||||
module.exports.RoomState = require("./models/room-state");
|
||||
/** The {@link module:models/user~User|User} class. */
|
||||
module.exports.User = require("./models/user");
|
||||
/** The {@link module:scheduler~MatrixScheduler|MatrixScheduler} class. */
|
||||
module.exports.MatrixScheduler = require("./scheduler");
|
||||
/** The {@link module:store/session/webstorage~WebStorageSessionStore|
|
||||
* WebStorageSessionStore} class. <strong>Work in progress; unstable.</strong> */
|
||||
module.exports.WebStorageSessionStore = require("./store/session/webstorage");
|
||||
/** True if crypto libraries are being used on this client. */
|
||||
module.exports.CRYPTO_ENABLED = require("./client").CRYPTO_ENABLED;
|
||||
/** {@link module:content-repo|ContentRepo} utility functions. */
|
||||
module.exports.ContentRepo = require("./content-repo");
|
||||
/** The {@link module:filter~Filter|Filter} class. */
|
||||
module.exports.Filter = require("./filter");
|
||||
/** The {@link module:timeline-window~TimelineWindow} class. */
|
||||
module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
|
||||
|
||||
/**
|
||||
* Create a new Matrix Call.
|
||||
* @function
|
||||
* @param {module:client.MatrixClient} client The MatrixClient instance to use.
|
||||
* @param {string} roomId The room the call is in.
|
||||
* @return {module:webrtc/call~MatrixCall} The Matrix call or null if the browser
|
||||
* does not support WebRTC.
|
||||
*/
|
||||
module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCall;
|
||||
|
||||
// expose the underlying request object so different environments can use
|
||||
// different request libs (e.g. request or browser-request)
|
||||
var request;
|
||||
/**
|
||||
* The function used to perform HTTP requests. Only use this if you want to
|
||||
* use a different HTTP library, e.g. Angular's <code>$http</code>. This should
|
||||
* be set prior to calling {@link createClient}.
|
||||
* @param {requestFunction} r The request function to use.
|
||||
*/
|
||||
module.exports.request = function(r) {
|
||||
request = r;
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
|
||||
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
|
||||
* @param {(Object|string)} opts The configuration options for this client. If
|
||||
* this is a string, it is assumed to be the base URL. These configuration
|
||||
* options will be passed directly to {@link module:client~MatrixClient}.
|
||||
* @param {Object} opts.store If not set, defaults to
|
||||
* {@link module:store/memory.MatrixInMemoryStore}.
|
||||
* @param {Object} opts.scheduler If not set, defaults to
|
||||
* {@link module:scheduler~MatrixScheduler}.
|
||||
* @param {requestFunction} opts.request If not set, defaults to the function
|
||||
* supplied to {@link request} which defaults to the request module from NPM.
|
||||
* @return {MatrixClient} A new matrix client.
|
||||
* @see {@link module:client~MatrixClient} for the full list of options for
|
||||
* <code>opts</code>.
|
||||
*/
|
||||
module.exports.createClient = function(opts) {
|
||||
if (typeof opts === "string") {
|
||||
opts = {
|
||||
"baseUrl": opts
|
||||
};
|
||||
}
|
||||
opts.request = opts.request || request;
|
||||
opts.store = opts.store || new module.exports.MatrixInMemoryStore({
|
||||
localStorage: global.localStorage
|
||||
});
|
||||
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
|
||||
return new module.exports.MatrixClient(opts);
|
||||
};
|
||||
|
||||
/**
|
||||
* The request function interface for performing HTTP requests. This matches the
|
||||
* API for the {@link https://github.com/request/request#requestoptions-callback|
|
||||
* request NPM module}. The SDK will attempt to call this function in order to
|
||||
* perform an HTTP request.
|
||||
* @callback requestFunction
|
||||
* @param {Object} opts The options for this HTTP request.
|
||||
* @param {string} opts.uri The complete URI.
|
||||
* @param {string} opts.method The HTTP method.
|
||||
* @param {Object} opts.qs The query parameters to append to the URI.
|
||||
* @param {Object} opts.body The JSON-serializable object.
|
||||
* @param {boolean} opts.json True if this is a JSON request.
|
||||
* @param {Object} opts._matrix_opts The underlying options set for
|
||||
* {@link MatrixHttpApi}.
|
||||
* @param {requestCallback} callback The request callback.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The request callback interface for performing HTTP requests. This matches the
|
||||
* API for the {@link https://github.com/request/request#requestoptions-callback|
|
||||
* request NPM module}. The SDK will implement a callback which meets this
|
||||
* interface in order to handle the HTTP response.
|
||||
* @callback requestCallback
|
||||
* @param {Error} err The error if one occurred, else falsey.
|
||||
* @param {Object} response The HTTP response which consists of
|
||||
* <code>{statusCode: {Number}, headers: {Object}}</code>
|
||||
* @param {Object} body The parsed HTTP response body.
|
||||
*/
|
||||
@@ -1,119 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @module models/event-context
|
||||
*/
|
||||
|
||||
/**
|
||||
* Construct a new EventContext
|
||||
*
|
||||
* An eventcontext is used for circumstances such as search results, when we
|
||||
* have a particular event of interest, and a bunch of events before and after
|
||||
* it.
|
||||
*
|
||||
* It also stores pagination tokens for going backwards and forwards in the
|
||||
* timeline.
|
||||
*
|
||||
* @param {MatrixEvent} ourEvent the event at the centre of this context
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function EventContext(ourEvent) {
|
||||
this._timeline = [ourEvent];
|
||||
this._ourEventIndex = 0;
|
||||
this._paginateTokens = {b: null, f: null};
|
||||
|
||||
// this is used by MatrixClient to keep track of active requests
|
||||
this._paginateRequests = {b: null, f: null};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the main event of interest
|
||||
*
|
||||
* This is a convenience function for getTimeline()[getOurEventIndex()].
|
||||
*
|
||||
* @return {MatrixEvent} The event at the centre of this context.
|
||||
*/
|
||||
EventContext.prototype.getEvent = function() {
|
||||
return this._timeline[this._ourEventIndex];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the list of events in this context
|
||||
*
|
||||
* @return {Array} An array of MatrixEvents
|
||||
*/
|
||||
EventContext.prototype.getTimeline = function() {
|
||||
return this._timeline;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the index in the timeline of our event
|
||||
*
|
||||
* @return {Number}
|
||||
*/
|
||||
EventContext.prototype.getOurEventIndex = function() {
|
||||
return this._ourEventIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a pagination token.
|
||||
*
|
||||
* @param {boolean} backwards true to get the pagination token for going
|
||||
* backwards in time
|
||||
* @return {string}
|
||||
*/
|
||||
EventContext.prototype.getPaginateToken = function(backwards) {
|
||||
return this._paginateTokens[backwards ? 'b' : 'f'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a pagination token.
|
||||
*
|
||||
* Generally this will be used only by the matrix js sdk.
|
||||
*
|
||||
* @param {string} token pagination token
|
||||
* @param {boolean} backwards true to set the pagination token for going
|
||||
* backwards in time
|
||||
*/
|
||||
EventContext.prototype.setPaginateToken = function(token, backwards) {
|
||||
this._paginateTokens[backwards ? 'b' : 'f'] = token;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add more events to the timeline
|
||||
*
|
||||
* @param {Array} events new events, in timeline order
|
||||
* @param {boolean} atStart true to insert new events at the start
|
||||
*/
|
||||
EventContext.prototype.addEvents = function(events, atStart) {
|
||||
// TODO: should we share logic with Room.addEventsToTimeline?
|
||||
// Should Room even use EventContext?
|
||||
|
||||
if (atStart) {
|
||||
this._timeline = events.concat(this._timeline);
|
||||
this._ourEventIndex += events.length;
|
||||
} else {
|
||||
this._timeline = this._timeline.concat(events);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The EventContext class
|
||||
*/
|
||||
module.exports = EventContext;
|
||||
@@ -1,308 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @module models/event-timeline
|
||||
*/
|
||||
|
||||
var RoomState = require("./room-state");
|
||||
var utils = require("../utils");
|
||||
var MatrixEvent = require("./event").MatrixEvent;
|
||||
|
||||
/**
|
||||
* Construct a new EventTimeline
|
||||
*
|
||||
* <p>An EventTimeline represents a contiguous sequence of events in a room.
|
||||
*
|
||||
* <p>As well as keeping track of the events themselves, it stores the state of
|
||||
* the room at the beginning and end of the timeline, and pagination tokens for
|
||||
* going backwards and forwards in the timeline.
|
||||
*
|
||||
* <p>In order that clients can meaningfully maintain an index into a timeline,
|
||||
* the EventTimeline object tracks a 'baseIndex'. This starts at zero, but is
|
||||
* incremented when events are prepended to the timeline. The index of an event
|
||||
* relative to baseIndex therefore remains constant.
|
||||
*
|
||||
* <p>Once a timeline joins up with its neighbour, they are linked together into a
|
||||
* doubly-linked list.
|
||||
*
|
||||
* @param {string} roomId the ID of the room where this timeline came from
|
||||
* @constructor
|
||||
*/
|
||||
function EventTimeline(roomId) {
|
||||
this._roomId = roomId;
|
||||
this._events = [];
|
||||
this._baseIndex = 0;
|
||||
this._startState = new RoomState(roomId);
|
||||
this._startState.paginationToken = null;
|
||||
this._endState = new RoomState(roomId);
|
||||
this._endState.paginationToken = null;
|
||||
|
||||
this._prevTimeline = null;
|
||||
this._nextTimeline = null;
|
||||
|
||||
// this is used by client.js
|
||||
this._paginationRequests = {'b': null, 'f': null};
|
||||
|
||||
this._name = roomId + ":" + new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Symbolic constant for methods which take a 'direction' argument:
|
||||
* refers to the start of the timeline, or backwards in time.
|
||||
*/
|
||||
EventTimeline.BACKWARDS = "b";
|
||||
|
||||
/**
|
||||
* Symbolic constant for methods which take a 'direction' argument:
|
||||
* refers to the end of the timeline, or forwards in time.
|
||||
*/
|
||||
EventTimeline.FORWARDS = "f";
|
||||
|
||||
/**
|
||||
* Initialise the start and end state with the given events
|
||||
*
|
||||
* <p>This can only be called before any events are added.
|
||||
*
|
||||
* @param {MatrixEvent[]} stateEvents list of state events to initialise the
|
||||
* state with.
|
||||
* @throws {Error} if an attempt is made to call this after addEvent is called.
|
||||
*/
|
||||
EventTimeline.prototype.initialiseState = function(stateEvents) {
|
||||
if (this._events.length > 0) {
|
||||
throw new Error("Cannot initialise state after events are added");
|
||||
}
|
||||
|
||||
// we deep-copy the events here, in case they get changed later - we don't
|
||||
// want changes to the start state leaking through to the end state.
|
||||
var oldStateEvents = utils.map(
|
||||
utils.deepCopy(
|
||||
stateEvents.map(function(mxEvent) { return mxEvent.event; })
|
||||
), function(ev) { return new MatrixEvent(ev); });
|
||||
|
||||
this._startState.setStateEvents(oldStateEvents);
|
||||
this._endState.setStateEvents(stateEvents);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the ID of the room for this timeline
|
||||
* @return {string} room ID
|
||||
*/
|
||||
EventTimeline.prototype.getRoomId = function() {
|
||||
return this._roomId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the base index.
|
||||
*
|
||||
* <p>This is an index which is incremented when events are prepended to the
|
||||
* timeline. An individual event therefore stays at the same index in the array
|
||||
* relative to the base index (although note that a given event's index may
|
||||
* well be less than the base index, thus giving that event a negative relative
|
||||
* index).
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
EventTimeline.prototype.getBaseIndex = function() {
|
||||
return this._baseIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the list of events in this context
|
||||
*
|
||||
* @return {MatrixEvent[]} An array of MatrixEvents
|
||||
*/
|
||||
EventTimeline.prototype.getEvents = function() {
|
||||
return this._events;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the room state at the start/end of the timeline
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
|
||||
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
|
||||
* of the timeline.
|
||||
*
|
||||
* @return {RoomState} state at the start/end of the timeline
|
||||
*/
|
||||
EventTimeline.prototype.getState = function(direction) {
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
return this._startState;
|
||||
} else if (direction == EventTimeline.FORWARDS) {
|
||||
return this._endState;
|
||||
} else {
|
||||
throw new Error("Invalid direction '" + direction + "'");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a pagination token
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to get the pagination
|
||||
* token for going backwards in time; EventTimeline.FORWARDS to get the
|
||||
* pagination token for going forwards in time.
|
||||
*
|
||||
* @return {?string} pagination token
|
||||
*/
|
||||
EventTimeline.prototype.getPaginationToken = function(direction) {
|
||||
return this.getState(direction).paginationToken;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a pagination token
|
||||
*
|
||||
* @param {?string} token pagination token
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to set the pagination
|
||||
* token for going backwards in time; EventTimeline.FORWARDS to set the
|
||||
* pagination token for going forwards in time.
|
||||
*/
|
||||
EventTimeline.prototype.setPaginationToken = function(token, direction) {
|
||||
this.getState(direction).paginationToken = token;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the next timeline in the series
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to get the previous
|
||||
* timeline; EventTimeline.FORWARDS to get the next timeline.
|
||||
*
|
||||
* @return {?EventTimeline} previous or following timeline, if they have been
|
||||
* joined up.
|
||||
*/
|
||||
EventTimeline.prototype.getNeighbouringTimeline = function(direction) {
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
return this._prevTimeline;
|
||||
} else if (direction == EventTimeline.FORWARDS) {
|
||||
return this._nextTimeline;
|
||||
} else {
|
||||
throw new Error("Invalid direction '" + direction + "'");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the next timeline in the series
|
||||
*
|
||||
* @param {EventTimeline} neighbour previous/following timeline
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to set the previous
|
||||
* timeline; EventTimeline.FORWARDS to set the next timeline.
|
||||
*
|
||||
* @throws {Error} if an attempt is made to set the neighbouring timeline when
|
||||
* it is already set.
|
||||
*/
|
||||
EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction) {
|
||||
if (this.getNeighbouringTimeline(direction)) {
|
||||
throw new Error("timeline already has a neighbouring timeline - " +
|
||||
"cannot reset neighbour");
|
||||
}
|
||||
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
this._prevTimeline = neighbour;
|
||||
} else if (direction == EventTimeline.FORWARDS) {
|
||||
this._nextTimeline = neighbour;
|
||||
} else {
|
||||
throw new Error("Invalid direction '" + direction + "'");
|
||||
}
|
||||
|
||||
// make sure we don't try to paginate this timeline
|
||||
this.setPaginationToken(null, direction);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new event to the timeline, and update the state
|
||||
*
|
||||
* @param {MatrixEvent} event new event
|
||||
* @param {boolean} atStart true to insert new event at the start
|
||||
*/
|
||||
EventTimeline.prototype.addEvent = function(event, atStart) {
|
||||
var stateContext = atStart ? this._startState : this._endState;
|
||||
|
||||
setEventMetadata(event, stateContext, atStart);
|
||||
|
||||
// modify state
|
||||
if (event.isState()) {
|
||||
stateContext.setStateEvents([event]);
|
||||
// it is possible that the act of setting the state event means we
|
||||
// can set more metadata (specifically sender/target props), so try
|
||||
// it again if the prop wasn't previously set. It may also mean that
|
||||
// the sender/target is updated (if the event set was a room member event)
|
||||
// so we want to use the *updated* member (new avatar/name) instead.
|
||||
//
|
||||
// However, we do NOT want to do this on member events if we're going
|
||||
// back in time, else we'll set the .sender value for BEFORE the given
|
||||
// member event, whereas we want to set the .sender value for the ACTUAL
|
||||
// member event itself.
|
||||
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
|
||||
setEventMetadata(event, stateContext, atStart);
|
||||
}
|
||||
}
|
||||
|
||||
var insertIndex;
|
||||
|
||||
if (atStart) {
|
||||
insertIndex = 0;
|
||||
} else {
|
||||
insertIndex = this._events.length;
|
||||
}
|
||||
|
||||
this._events.splice(insertIndex, 0, event); // insert element
|
||||
if (atStart) {
|
||||
this._baseIndex++;
|
||||
}
|
||||
};
|
||||
|
||||
function setEventMetadata(event, stateContext, toStartOfTimeline) {
|
||||
// set sender and target properties
|
||||
event.sender = stateContext.getSentinelMember(
|
||||
event.getSender()
|
||||
);
|
||||
if (event.getType() === "m.room.member") {
|
||||
event.target = stateContext.getSentinelMember(
|
||||
event.getStateKey()
|
||||
);
|
||||
}
|
||||
if (event.isState()) {
|
||||
// room state has no concept of 'old' or 'current', but we want the
|
||||
// room state to regress back to previous values if toStartOfTimeline
|
||||
// is set, which means inspecting prev_content if it exists. This
|
||||
// is done by toggling the forwardLooking flag.
|
||||
if (toStartOfTimeline) {
|
||||
event.forwardLooking = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an event from the timeline
|
||||
*
|
||||
* @param {string} eventId ID of event to be removed
|
||||
* @return {?MatrixEvent} removed event, or null if not found
|
||||
*/
|
||||
EventTimeline.prototype.removeEvent = function(eventId) {
|
||||
for (var i = this._events.length - 1; i >= 0; i--) {
|
||||
var ev = this._events[i];
|
||||
if (ev.getId() == eventId) {
|
||||
this._events.splice(i, 1);
|
||||
if (i < this._baseIndex) {
|
||||
this._baseIndex--;
|
||||
}
|
||||
return ev;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a string to identify this timeline, for debugging
|
||||
*
|
||||
* @return {string} name for this timeline
|
||||
*/
|
||||
EventTimeline.prototype.toString = function() {
|
||||
return this._name;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The EventTimeline class
|
||||
*/
|
||||
module.exports = EventTimeline;
|
||||
@@ -1,278 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for
|
||||
* the public classes.
|
||||
* @module models/event
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enum for event statuses.
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
module.exports.EventStatus = {
|
||||
/** The event was not sent and will no longer be retried. */
|
||||
NOT_SENT: "not_sent",
|
||||
/** The event is in the process of being sent. */
|
||||
SENDING: "sending",
|
||||
/** The event is in a queue waiting to be sent. */
|
||||
QUEUED: "queued",
|
||||
/** The event has been sent to the server, but we have not yet received the
|
||||
* echo. */
|
||||
SENT: "sent",
|
||||
|
||||
/** The event was cancelled before it was successfully sent. */
|
||||
CANCELLED: "cancelled",
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a Matrix Event object
|
||||
* @constructor
|
||||
* @param {Object} event The raw event to be wrapped in this DAO
|
||||
* @param {boolean} encrypted Was the event encrypted
|
||||
* @prop {Object} event The raw event. <b>Do not access this property</b>
|
||||
* directly unless you absolutely have to. Prefer the getter methods defined on
|
||||
* this class. Using the getter methods shields your app from
|
||||
* changes to event JSON between Matrix versions.
|
||||
* @prop {RoomMember} sender The room member who sent this event, or null e.g.
|
||||
* this is a presence event.
|
||||
* @prop {RoomMember} target The room member who is the target of this event, e.g.
|
||||
* the invitee, the person being banned, etc.
|
||||
* @prop {EventStatus} status The sending status of the event.
|
||||
* @prop {boolean} forwardLooking True if this event is 'forward looking', meaning
|
||||
* that getDirectionalContent() will return event.content and not event.prev_content.
|
||||
* Default: true. <strong>This property is experimental and may change.</strong>
|
||||
*/
|
||||
module.exports.MatrixEvent = function MatrixEvent(event, encrypted) {
|
||||
this.event = event || {};
|
||||
this.sender = null;
|
||||
this.target = null;
|
||||
this.status = null;
|
||||
this.forwardLooking = true;
|
||||
this.encrypted = Boolean(encrypted);
|
||||
};
|
||||
module.exports.MatrixEvent.prototype = {
|
||||
|
||||
/**
|
||||
* Get the event_id for this event.
|
||||
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
|
||||
* </code>
|
||||
*/
|
||||
getId: function() {
|
||||
return this.event.event_id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the user_id for this event.
|
||||
* @return {string} The user ID, e.g. <code>@alice:matrix.org</code>
|
||||
*/
|
||||
getSender: function() {
|
||||
return this.event.sender || this.event.user_id; // v2 / v1
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the type of event.
|
||||
* @return {string} The event type, e.g. <code>m.room.message</code>
|
||||
*/
|
||||
getType: function() {
|
||||
return this.event.type;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the type of the event that will be sent to the homeserver.
|
||||
* @return {string} The event type.
|
||||
*/
|
||||
getWireType: function() {
|
||||
return this.encryptedType || this.event.type;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the room_id for this event. This will return <code>undefined</code>
|
||||
* for <code>m.presence</code> events.
|
||||
* @return {string} The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
|
||||
* </code>
|
||||
*/
|
||||
getRoomId: function() {
|
||||
return this.event.room_id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the timestamp of this event.
|
||||
* @return {Number} The event timestamp, e.g. <code>1433502692297</code>
|
||||
*/
|
||||
getTs: function() {
|
||||
return this.event.origin_server_ts;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the event content JSON.
|
||||
* @return {Object} The event content JSON, or an empty object.
|
||||
*/
|
||||
getContent: function() {
|
||||
return this.event.content || {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the event content JSON that will be sent to the homeserver.
|
||||
* @return {Object} The event content JSON, or an empty object.
|
||||
*/
|
||||
getWireContent: function() {
|
||||
return this.encryptedContent || this.event.content || {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the previous event content JSON. This will only return something for
|
||||
* state events which exist in the timeline.
|
||||
* @return {Object} The previous event content JSON, or an empty object.
|
||||
*/
|
||||
getPrevContent: function() {
|
||||
// v2 then v1 then default
|
||||
return this.getUnsigned().prev_content || this.event.prev_content || {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get either 'content' or 'prev_content' depending on if this event is
|
||||
* 'forward-looking' or not. This can be modified via event.forwardLooking.
|
||||
* In practice, this means we get the chronologically earlier content value
|
||||
* for this event (this method should surely be called getEarlierContent)
|
||||
* <strong>This method is experimental and may change.</strong>
|
||||
* @return {Object} event.content if this event is forward-looking, else
|
||||
* event.prev_content.
|
||||
*/
|
||||
getDirectionalContent: function() {
|
||||
return this.forwardLooking ? this.getContent() : this.getPrevContent();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the age of this event. This represents the age of the event when the
|
||||
* event arrived at the device, and not the age of the event when this
|
||||
* function was called.
|
||||
* @return {Number} The age of this event in milliseconds.
|
||||
*/
|
||||
getAge: function() {
|
||||
return this.getUnsigned().age || this.event.age; // v2 / v1
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the event state_key if it has one. This will return <code>undefined
|
||||
* </code> for message events.
|
||||
* @return {string} The event's <code>state_key</code>.
|
||||
*/
|
||||
getStateKey: function() {
|
||||
return this.event.state_key;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if this event is a state event.
|
||||
* @return {boolean} True if this is a state event.
|
||||
*/
|
||||
isState: function() {
|
||||
return this.event.state_key !== undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the event is encrypted.
|
||||
* @return {boolean} True if this event is encrypted.
|
||||
*/
|
||||
isEncrypted: function() {
|
||||
return this.encrypted;
|
||||
},
|
||||
|
||||
getUnsigned: function() {
|
||||
return this.event.unsigned || {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the content of an event in the same way it would be by the server
|
||||
* if it were redacted before it was sent to us
|
||||
*
|
||||
* @param {Object} the raw event causing the redaction
|
||||
*/
|
||||
makeRedacted: function(redaction_event) {
|
||||
if (!this.event.unsigned) {
|
||||
this.event.unsigned = {};
|
||||
}
|
||||
this.event.unsigned.redacted_because = redaction_event;
|
||||
|
||||
var key;
|
||||
for (key in this.event) {
|
||||
if (!this.event.hasOwnProperty(key)) { continue; }
|
||||
if (!_REDACT_KEEP_KEY_MAP[key]) {
|
||||
delete this.event[key];
|
||||
}
|
||||
}
|
||||
|
||||
var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
|
||||
for (key in this.event.content) {
|
||||
if (!this.event.content.hasOwnProperty(key)) { continue; }
|
||||
if (!keeps[key]) {
|
||||
delete this.event.content[key];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if this event has been redacted
|
||||
*
|
||||
* @return {boolean} True if this event has been redacted
|
||||
*/
|
||||
isRedacted: function() {
|
||||
return Boolean(this.getUnsigned().redacted_because);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says:
|
||||
*
|
||||
* the server should strip off any keys not in the following list:
|
||||
* event_id
|
||||
* type
|
||||
* room_id
|
||||
* user_id
|
||||
* state_key
|
||||
* prev_state
|
||||
* content
|
||||
* [we keep 'unsigned' as well, since that is created by the local server]
|
||||
*
|
||||
* The content object should also be stripped of all keys, unless it is one of
|
||||
* one of the following event types:
|
||||
* m.room.member allows key membership
|
||||
* m.room.create allows key creator
|
||||
* m.room.join_rules allows key join_rule
|
||||
* m.room.power_levels allows keys ban, events, events_default, kick,
|
||||
* redact, state_default, users, users_default.
|
||||
* m.room.aliases allows key aliases
|
||||
*/
|
||||
// a map giving the keys we keep when an event is redacted
|
||||
var _REDACT_KEEP_KEY_MAP = [
|
||||
'event_id', 'type', 'room_id', 'user_id', 'state_key', 'prev_state',
|
||||
'content', 'unsigned',
|
||||
].reduce(function(ret, val) { ret[val] = 1; return ret; }, {});
|
||||
|
||||
// a map from event type to the .content keys we keep when an event is redacted
|
||||
var _REDACT_KEEP_CONTENT_MAP = {
|
||||
'm.room.member': {'membership': 1},
|
||||
'm.room.create': {'creator': 1},
|
||||
'm.room.join_rules': {'join_rule': 1},
|
||||
'm.room.power_levels': {'ban': 1, 'events': 1, 'events_default': 1,
|
||||
'kick': 1, 'redact': 1, 'state_default': 1,
|
||||
'users': 1, 'users_default': 1,
|
||||
},
|
||||
'm.room.aliases': {'aliases': 1},
|
||||
};
|
||||
@@ -1,290 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
/**
|
||||
* @module models/room-member
|
||||
*/
|
||||
var EventEmitter = require("events").EventEmitter;
|
||||
var ContentRepo = require("../content-repo");
|
||||
|
||||
var utils = require("../utils");
|
||||
|
||||
/**
|
||||
* Construct a new room member.
|
||||
* @constructor
|
||||
* @param {string} roomId The room ID of the member.
|
||||
* @param {string} userId The user ID of the member.
|
||||
* @prop {string} roomId The room ID for this member.
|
||||
* @prop {string} userId The user ID of this member.
|
||||
* @prop {boolean} typing True if the room member is currently typing.
|
||||
* @prop {string} name The human-readable name for this room member.
|
||||
* @prop {Number} powerLevel The power level for this room member.
|
||||
* @prop {Number} powerLevelNorm The normalised power level (0-100) for this
|
||||
* room member.
|
||||
* @prop {User} user The User object for this room member, if one exists.
|
||||
* @prop {string} membership The membership state for this room member e.g. 'join'.
|
||||
* @prop {Object} events The events describing this RoomMember.
|
||||
* @prop {MatrixEvent} events.member The m.room.member event for this RoomMember.
|
||||
*/
|
||||
function RoomMember(roomId, userId) {
|
||||
this.roomId = roomId;
|
||||
this.userId = userId;
|
||||
this.typing = false;
|
||||
this.name = userId;
|
||||
this.powerLevel = 0;
|
||||
this.powerLevelNorm = 0;
|
||||
this.user = null;
|
||||
this.membership = null;
|
||||
this.events = {
|
||||
member: null
|
||||
};
|
||||
this._updateModifiedTime();
|
||||
}
|
||||
utils.inherits(RoomMember, EventEmitter);
|
||||
|
||||
/**
|
||||
* Update this room member's membership event. May fire "RoomMember.name" if
|
||||
* this event updates this member's name.
|
||||
* @param {MatrixEvent} event The <code>m.room.member</code> event
|
||||
* @param {RoomState} roomState Optional. The room state to take into account
|
||||
* when calculating (e.g. for disambiguating users with the same name).
|
||||
* @fires module:client~MatrixClient#event:"RoomMember.name"
|
||||
* @fires module:client~MatrixClient#event:"RoomMember.membership"
|
||||
*/
|
||||
RoomMember.prototype.setMembershipEvent = function(event, roomState) {
|
||||
if (event.getType() !== "m.room.member") {
|
||||
return;
|
||||
}
|
||||
this.events.member = event;
|
||||
|
||||
var oldMembership = this.membership;
|
||||
this.membership = event.getDirectionalContent().membership;
|
||||
|
||||
var oldName = this.name;
|
||||
this.name = calculateDisplayName(this, event, roomState);
|
||||
if (oldMembership !== this.membership) {
|
||||
this._updateModifiedTime();
|
||||
this.emit("RoomMember.membership", event, this);
|
||||
}
|
||||
if (oldName !== this.name) {
|
||||
this._updateModifiedTime();
|
||||
this.emit("RoomMember.name", event, this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update this room member's power level event. May fire
|
||||
* "RoomMember.powerLevel" if this event updates this member's power levels.
|
||||
* @param {MatrixEvent} powerLevelEvent The <code>m.room.power_levels</code>
|
||||
* event
|
||||
* @fires module:client~MatrixClient#event:"RoomMember.powerLevel"
|
||||
*/
|
||||
RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) {
|
||||
if (powerLevelEvent.getType() !== "m.room.power_levels") {
|
||||
return;
|
||||
}
|
||||
var maxLevel = powerLevelEvent.getContent().users_default || 0;
|
||||
utils.forEach(utils.values(powerLevelEvent.getContent().users), function(lvl) {
|
||||
maxLevel = Math.max(maxLevel, lvl);
|
||||
});
|
||||
var oldPowerLevel = this.powerLevel;
|
||||
var oldPowerLevelNorm = this.powerLevelNorm;
|
||||
this.powerLevel = (
|
||||
powerLevelEvent.getContent().users[this.userId] ||
|
||||
powerLevelEvent.getContent().users_default ||
|
||||
0
|
||||
);
|
||||
this.powerLevelNorm = 0;
|
||||
if (maxLevel > 0) {
|
||||
this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
|
||||
}
|
||||
|
||||
// emit for changes in powerLevelNorm as well (since the app will need to
|
||||
// redraw everyone's level if the max has changed)
|
||||
if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) {
|
||||
this._updateModifiedTime();
|
||||
this.emit("RoomMember.powerLevel", powerLevelEvent, this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update this room member's typing event. May fire "RoomMember.typing" if
|
||||
* this event changes this member's typing state.
|
||||
* @param {MatrixEvent} event The typing event
|
||||
* @fires module:client~MatrixClient#event:"RoomMember.typing"
|
||||
*/
|
||||
RoomMember.prototype.setTypingEvent = function(event) {
|
||||
if (event.getType() !== "m.typing") {
|
||||
return;
|
||||
}
|
||||
var oldTyping = this.typing;
|
||||
this.typing = false;
|
||||
var typingList = event.getContent().user_ids;
|
||||
if (!utils.isArray(typingList)) {
|
||||
// malformed event :/ bail early. TODO: whine?
|
||||
return;
|
||||
}
|
||||
if (typingList.indexOf(this.userId) !== -1) {
|
||||
this.typing = true;
|
||||
}
|
||||
if (oldTyping !== this.typing) {
|
||||
this._updateModifiedTime();
|
||||
this.emit("RoomMember.typing", event, this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the last modified time to the current time.
|
||||
*/
|
||||
RoomMember.prototype._updateModifiedTime = function() {
|
||||
this._modified = Date.now();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the timestamp when this RoomMember was last updated. This timestamp is
|
||||
* updated when properties on this RoomMember are updated.
|
||||
* It is updated <i>before</i> firing events.
|
||||
* @return {number} The timestamp
|
||||
*/
|
||||
RoomMember.prototype.getLastModifiedTime = function() {
|
||||
return this._modified;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the avatar URL for a room member.
|
||||
* @param {string} baseUrl The base homeserver URL See
|
||||
* {@link module:client~MatrixClient#getHomeserverUrl}.
|
||||
* @param {Number} width The desired width of the thumbnail.
|
||||
* @param {Number} height The desired height of the thumbnail.
|
||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||
* "crop" or "scale".
|
||||
* @param {Boolean} allowDefault (optional) Passing false causes this method to
|
||||
* return null if the user has no avatar image. Otherwise, a default image URL
|
||||
* will be returned. Default: true.
|
||||
* @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be
|
||||
* returned even if it is a direct hyperlink rather than a matrix content URL.
|
||||
* If false, any non-matrix content URLs will be ignored. Setting this option to
|
||||
* true will expose URLs that, if fetched, will leak information about the user
|
||||
* to anyone who they share a room with.
|
||||
* @return {?string} the avatar URL or null.
|
||||
*/
|
||||
RoomMember.prototype.getAvatarUrl =
|
||||
function(baseUrl, width, height, resizeMethod, allowDefault, allowDirectLinks) {
|
||||
if (allowDefault === undefined) { allowDefault = true; }
|
||||
if (!this.events.member && !allowDefault) {
|
||||
return null;
|
||||
}
|
||||
var rawUrl = this.events.member ? this.events.member.getContent().avatar_url : null;
|
||||
var httpUrl = ContentRepo.getHttpUriForMxc(
|
||||
baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks
|
||||
);
|
||||
if (httpUrl) {
|
||||
return httpUrl;
|
||||
}
|
||||
else if (allowDefault) {
|
||||
return ContentRepo.getIdenticonUri(
|
||||
baseUrl, this.userId, width, height
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
function calculateDisplayName(member, event, roomState) {
|
||||
var displayName = event.getDirectionalContent().displayname;
|
||||
var selfUserId = member.userId;
|
||||
|
||||
/*
|
||||
// FIXME: this would be great but still needs to use the
|
||||
// full userId to disambiguate if needed...
|
||||
|
||||
if (!displayName) {
|
||||
var matches = selfUserId.match(/^@(.*?):/);
|
||||
if (matches) {
|
||||
return matches[1];
|
||||
}
|
||||
else {
|
||||
return selfUserId;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
if (!displayName) {
|
||||
return selfUserId;
|
||||
}
|
||||
|
||||
if (!roomState) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
var userIds = roomState.getUserIdsWithDisplayName(displayName);
|
||||
var otherUsers = userIds.filter(function(u) {
|
||||
return u !== selfUserId;
|
||||
});
|
||||
if (otherUsers.length > 0) {
|
||||
return displayName + " (" + selfUserId + ")";
|
||||
}
|
||||
return displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* The RoomMember class.
|
||||
*/
|
||||
module.exports = RoomMember;
|
||||
|
||||
/**
|
||||
* Fires whenever any room member's name changes.
|
||||
* @event module:client~MatrixClient#"RoomMember.name"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomMember} member The member whose RoomMember.name changed.
|
||||
* @example
|
||||
* matrixClient.on("RoomMember.name", function(event, member){
|
||||
* var newName = member.name;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever any room member's membership state changes.
|
||||
* @event module:client~MatrixClient#"RoomMember.membership"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomMember} member The member whose RoomMember.membership changed.
|
||||
* @example
|
||||
* matrixClient.on("RoomMember.membership", function(event, member){
|
||||
* var newState = member.membership;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever any room member's typing state changes.
|
||||
* @event module:client~MatrixClient#"RoomMember.typing"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomMember} member The member whose RoomMember.typing changed.
|
||||
* @example
|
||||
* matrixClient.on("RoomMember.typing", function(event, member){
|
||||
* var isTyping = member.typing;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever any room member's power level changes.
|
||||
* @event module:client~MatrixClient#"RoomMember.powerLevel"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomMember} member The member whose RoomMember.powerLevel changed.
|
||||
* @example
|
||||
* matrixClient.on("RoomMember.powerLevel", function(event, member){
|
||||
* var newPowerLevel = member.powerLevel;
|
||||
* var newNormPowerLevel = member.powerLevelNorm;
|
||||
* });
|
||||
*/
|
||||
@@ -1,431 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
/**
|
||||
* @module models/room-state
|
||||
*/
|
||||
var EventEmitter = require("events").EventEmitter;
|
||||
|
||||
var utils = require("../utils");
|
||||
var RoomMember = require("./room-member");
|
||||
|
||||
/**
|
||||
* Construct room state.
|
||||
* @constructor
|
||||
* @param {string} roomId Required. The ID of the room which has this state.
|
||||
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
|
||||
* on the user's ID.
|
||||
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
|
||||
* events dictionary, keyed on the event type and then the state_key value.
|
||||
* @prop {string} paginationToken The pagination token for this state.
|
||||
*/
|
||||
function RoomState(roomId) {
|
||||
this.roomId = roomId;
|
||||
this.members = {
|
||||
// userId: RoomMember
|
||||
};
|
||||
this.events = {
|
||||
// eventType: { stateKey: MatrixEvent }
|
||||
};
|
||||
this.paginationToken = null;
|
||||
|
||||
this._sentinels = {
|
||||
// userId: RoomMember
|
||||
};
|
||||
this._updateModifiedTime();
|
||||
this._displayNameToUserIds = {};
|
||||
this._userIdsToDisplayNames = {};
|
||||
this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite
|
||||
}
|
||||
utils.inherits(RoomState, EventEmitter);
|
||||
|
||||
/**
|
||||
* Get all RoomMembers in this room.
|
||||
* @return {Array<RoomMember>} A list of RoomMembers.
|
||||
*/
|
||||
RoomState.prototype.getMembers = function() {
|
||||
return utils.values(this.members);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a room member by their user ID.
|
||||
* @param {string} userId The room member's user ID.
|
||||
* @return {RoomMember} The member or null if they do not exist.
|
||||
*/
|
||||
RoomState.prototype.getMember = function(userId) {
|
||||
return this.members[userId] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a room member whose properties will not change with this room state. You
|
||||
* typically want this if you want to attach a RoomMember to a MatrixEvent which
|
||||
* may no longer be represented correctly by Room.currentState or Room.oldState.
|
||||
* The term 'sentinel' refers to the fact that this RoomMember is an unchanging
|
||||
* guardian for state at this particular point in time.
|
||||
* @param {string} userId The room member's user ID.
|
||||
* @return {RoomMember} The member or null if they do not exist.
|
||||
*/
|
||||
RoomState.prototype.getSentinelMember = function(userId) {
|
||||
return this._sentinels[userId] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get state events from the state of the room.
|
||||
* @param {string} eventType The event type of the state event.
|
||||
* @param {string} stateKey Optional. The state_key of the state event. If
|
||||
* this is <code>undefined</code> then all matching state events will be
|
||||
* returned.
|
||||
* @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was
|
||||
* <code>undefined</code>, else a single event (or null if no match found).
|
||||
*/
|
||||
RoomState.prototype.getStateEvents = function(eventType, stateKey) {
|
||||
if (!this.events[eventType]) {
|
||||
// no match
|
||||
return stateKey === undefined ? [] : null;
|
||||
}
|
||||
if (stateKey === undefined) { // return all values
|
||||
return utils.values(this.events[eventType]);
|
||||
}
|
||||
var event = this.events[eventType][stateKey];
|
||||
return event ? event : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an array of one or more state MatrixEvents, overwriting
|
||||
* any existing state with the same {type, stateKey} tuple. Will fire
|
||||
* "RoomState.events" for every event added. May fire "RoomState.members"
|
||||
* if there are <code>m.room.member</code> events.
|
||||
* @param {MatrixEvent[]} stateEvents a list of state events for this room.
|
||||
* @fires module:client~MatrixClient#event:"RoomState.members"
|
||||
* @fires module:client~MatrixClient#event:"RoomState.newMember"
|
||||
* @fires module:client~MatrixClient#event:"RoomState.events"
|
||||
*/
|
||||
RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
var self = this;
|
||||
this._updateModifiedTime();
|
||||
|
||||
// update the core event dict
|
||||
utils.forEach(stateEvents, function(event) {
|
||||
if (event.getRoomId() !== self.roomId) { return; }
|
||||
if (!event.isState()) { return; }
|
||||
|
||||
if (self.events[event.getType()] === undefined) {
|
||||
self.events[event.getType()] = {};
|
||||
}
|
||||
self.events[event.getType()][event.getStateKey()] = event;
|
||||
if (event.getType() === "m.room.member") {
|
||||
_updateDisplayNameCache(
|
||||
self, event.getStateKey(), event.getContent().displayname
|
||||
);
|
||||
_updateThirdPartyTokenCache(self, event);
|
||||
}
|
||||
self.emit("RoomState.events", event, self);
|
||||
});
|
||||
|
||||
// update higher level data structures. This needs to be done AFTER the
|
||||
// core event dict as these structures may depend on other state events in
|
||||
// the given array (e.g. disambiguating display names in one go to do both
|
||||
// clashing names rather than progressively which only catches 1 of them).
|
||||
utils.forEach(stateEvents, function(event) {
|
||||
if (event.getRoomId() !== self.roomId) { return; }
|
||||
if (!event.isState()) { return; }
|
||||
|
||||
if (event.getType() === "m.room.member") {
|
||||
var userId = event.getStateKey();
|
||||
|
||||
// leave events apparently elide the displayname or avatar_url,
|
||||
// so let's fake one up so that we don't leak user ids
|
||||
// into the timeline
|
||||
if (event.getContent().membership === "leave" ||
|
||||
event.getContent().membership === "ban")
|
||||
{
|
||||
event.getContent().avatar_url =
|
||||
event.getContent().avatar_url ||
|
||||
event.getPrevContent().avatar_url;
|
||||
event.getContent().displayname =
|
||||
event.getContent().displayname ||
|
||||
event.getPrevContent().displayname;
|
||||
}
|
||||
|
||||
var member = self.members[userId];
|
||||
if (!member) {
|
||||
member = new RoomMember(event.getRoomId(), userId);
|
||||
self.emit("RoomState.newMember", event, self, member);
|
||||
}
|
||||
// Add a new sentinel for this change. We apply the same
|
||||
// operations to both sentinel and member rather than deep copying
|
||||
// so we don't make assumptions about the properties of RoomMember
|
||||
// (e.g. and manage to break it because deep copying doesn't do
|
||||
// everything).
|
||||
var sentinel = new RoomMember(event.getRoomId(), userId);
|
||||
utils.forEach([member, sentinel], function(roomMember) {
|
||||
roomMember.setMembershipEvent(event, self);
|
||||
// this member may have a power level already, so set it.
|
||||
var pwrLvlEvent = self.getStateEvents("m.room.power_levels", "");
|
||||
if (pwrLvlEvent) {
|
||||
roomMember.setPowerLevelEvent(pwrLvlEvent);
|
||||
}
|
||||
});
|
||||
|
||||
self._sentinels[userId] = sentinel;
|
||||
self.members[userId] = member;
|
||||
self.emit("RoomState.members", event, self, member);
|
||||
}
|
||||
else if (event.getType() === "m.room.power_levels") {
|
||||
var members = utils.values(self.members);
|
||||
utils.forEach(members, function(member) {
|
||||
member.setPowerLevelEvent(event);
|
||||
self.emit("RoomState.members", event, self, member);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the current typing event for this room.
|
||||
* @param {MatrixEvent} event The typing event
|
||||
*/
|
||||
RoomState.prototype.setTypingEvent = function(event) {
|
||||
utils.forEach(utils.values(this.members), function(member) {
|
||||
member.setTypingEvent(event);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the m.room.member event which has the given third party invite token.
|
||||
*
|
||||
* @param {string} token The token
|
||||
* @return {?MatrixEvent} The m.room.member event or null
|
||||
*/
|
||||
RoomState.prototype.getInviteForThreePidToken = function(token) {
|
||||
return this._tokenToInvite[token] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the last modified time to the current time.
|
||||
*/
|
||||
RoomState.prototype._updateModifiedTime = function() {
|
||||
this._modified = Date.now();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the timestamp when this room state was last updated. This timestamp is
|
||||
* updated when this object has received new state events.
|
||||
* @return {number} The timestamp
|
||||
*/
|
||||
RoomState.prototype.getLastModifiedTime = function() {
|
||||
return this._modified;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get user IDs with the specified display name.
|
||||
* @param {string} displayName The display name to get user IDs from.
|
||||
* @return {string[]} An array of user IDs or an empty array.
|
||||
*/
|
||||
RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
|
||||
return this._displayNameToUserIds[displayName] || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Short-form for maySendEvent('m.room.message', userId)
|
||||
* @param {string} userId The user ID of the user to test permission for
|
||||
* @return {boolean} true if the given user ID should be permitted to send
|
||||
* message events into the given room.
|
||||
*/
|
||||
RoomState.prototype.maySendMessage = function(userId) {
|
||||
return this._maySendEventOfType('m.room.message', userId, false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the given user ID has permission to send a normal
|
||||
* event of type `eventType` into this room.
|
||||
* @param {string} type The type of event to test
|
||||
* @param {string} userId The user ID of the user to test permission for
|
||||
* @return {boolean} true if the given user ID should be permitted to send
|
||||
* the given type of event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
RoomState.prototype.maySendEvent = function(eventType, userId) {
|
||||
return this._maySendEventOfType(eventType, userId, false);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the given MatrixClient has permission to send a state
|
||||
* event of type `stateEventType` into this room.
|
||||
* @param {string} type The type of state events to test
|
||||
* @param {MatrixClient} The client to test permission for
|
||||
* @return {boolean} true if the given client should be permitted to send
|
||||
* the given type of state event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
RoomState.prototype.mayClientSendStateEvent = function(stateEventType, cli) {
|
||||
if (cli.isGuest()) {
|
||||
return false;
|
||||
}
|
||||
return this.maySendStateEvent(stateEventType, cli.credentials.userId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the given user ID has permission to send a state
|
||||
* event of type `stateEventType` into this room.
|
||||
* @param {string} type The type of state events to test
|
||||
* @param {string} userId The user ID of the user to test permission for
|
||||
* @return {boolean} true if the given user ID should be permitted to send
|
||||
* the given type of state event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
|
||||
return this._maySendEventOfType(stateEventType, userId, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the given user ID has permission to send a normal or state
|
||||
* event of type `eventType` into this room.
|
||||
* @param {string} type The type of event to test
|
||||
* @param {string} userId The user ID of the user to test permission for
|
||||
* @param {boolean} state If true, tests if the user may send a state
|
||||
event of this type. Otherwise tests whether
|
||||
they may send a regular event.
|
||||
* @return {boolean} true if the given user ID should be permitted to send
|
||||
* the given type of event into this room,
|
||||
* according to the room's state.
|
||||
*/
|
||||
RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
|
||||
var member = this.getMember(userId);
|
||||
if (!member || member.membership == 'leave') { return false; }
|
||||
|
||||
var power_levels_event = this.getStateEvents('m.room.power_levels', '');
|
||||
|
||||
var power_levels;
|
||||
var events_levels = {};
|
||||
|
||||
var default_user_level = 0;
|
||||
var user_levels = [];
|
||||
|
||||
var state_default = 0;
|
||||
var events_default = 0;
|
||||
if (power_levels_event) {
|
||||
power_levels = power_levels_event.getContent();
|
||||
events_levels = power_levels.events || {};
|
||||
|
||||
default_user_level = parseInt(power_levels.users_default || 0);
|
||||
user_levels = power_levels.users || {};
|
||||
|
||||
if (power_levels.state_default !== undefined) {
|
||||
state_default = power_levels.state_default;
|
||||
} else {
|
||||
state_default = 50;
|
||||
}
|
||||
if (power_levels.events_default !== undefined) {
|
||||
events_default = power_levels.events_default;
|
||||
}
|
||||
}
|
||||
|
||||
var required_level = state ? state_default : events_default;
|
||||
if (events_levels[eventType] !== undefined) {
|
||||
required_level = events_levels[eventType];
|
||||
}
|
||||
return member.powerLevel >= required_level;
|
||||
};
|
||||
|
||||
/**
|
||||
* The RoomState class.
|
||||
*/
|
||||
module.exports = RoomState;
|
||||
|
||||
|
||||
function _updateThirdPartyTokenCache(roomState, memberEvent) {
|
||||
if (!memberEvent.getContent().third_party_invite) {
|
||||
return;
|
||||
}
|
||||
var token = (memberEvent.getContent().third_party_invite.signed || {}).token;
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
var threePidInvite = roomState.getStateEvents(
|
||||
"m.room.third_party_invite", token
|
||||
);
|
||||
if (!threePidInvite) {
|
||||
return;
|
||||
}
|
||||
roomState._tokenToInvite[token] = memberEvent;
|
||||
}
|
||||
|
||||
function _updateDisplayNameCache(roomState, userId, displayName) {
|
||||
var oldName = roomState._userIdsToDisplayNames[userId];
|
||||
delete roomState._userIdsToDisplayNames[userId];
|
||||
if (oldName) {
|
||||
// Remove the old name from the cache.
|
||||
// We clobber the user_id > name lookup but the name -> [user_id] lookup
|
||||
// means we need to remove that user ID from that array rather than nuking
|
||||
// the lot.
|
||||
var existingUserIds = roomState._displayNameToUserIds[oldName] || [];
|
||||
for (var i = 0; i < existingUserIds.length; i++) {
|
||||
if (existingUserIds[i] === userId) {
|
||||
// remove this user ID from this array
|
||||
existingUserIds.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
roomState._displayNameToUserIds[oldName] = existingUserIds;
|
||||
}
|
||||
|
||||
roomState._userIdsToDisplayNames[userId] = displayName;
|
||||
if (!roomState._displayNameToUserIds[displayName]) {
|
||||
roomState._displayNameToUserIds[displayName] = [];
|
||||
}
|
||||
roomState._displayNameToUserIds[displayName].push(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires whenever the event dictionary in room state is updated.
|
||||
* @event module:client~MatrixClient#"RoomState.events"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomState} state The room state whose RoomState.events dictionary
|
||||
* was updated.
|
||||
* @example
|
||||
* matrixClient.on("RoomState.events", function(event, state){
|
||||
* var newStateEvent = event;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever a member in the members dictionary is updated in any way.
|
||||
* @event module:client~MatrixClient#"RoomState.members"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomState} state The room state whose RoomState.members dictionary
|
||||
* was updated.
|
||||
* @param {RoomMember} member The room member that was updated.
|
||||
* @example
|
||||
* matrixClient.on("RoomState.members", function(event, state, member){
|
||||
* var newMembershipState = member.membership;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever a member is added to the members dictionary. The RoomMember
|
||||
* will not be fully populated yet (e.g. no membership state).
|
||||
* @event module:client~MatrixClient#"RoomState.newMember"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomState} state The room state whose RoomState.members dictionary
|
||||
* was updated with a new entry.
|
||||
* @param {RoomMember} member The room member that was added.
|
||||
* @example
|
||||
* matrixClient.on("RoomState.newMember", function(event, state, member){
|
||||
* // add event listeners on 'member'
|
||||
* });
|
||||
*/
|
||||
-1581
File diff suppressed because it is too large
Load Diff
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @module models/search-result
|
||||
*/
|
||||
|
||||
var EventContext = require("./event-context");
|
||||
var utils = require("../utils");
|
||||
|
||||
/**
|
||||
* Construct a new SearchResult
|
||||
*
|
||||
* @param {number} rank where this SearchResult ranks in the results
|
||||
* @param {event-context.EventContext} eventContext the matching event and its
|
||||
* context
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function SearchResult(rank, eventContext) {
|
||||
this.rank = rank;
|
||||
this.context = eventContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SearchResponse from the response to /search
|
||||
* @static
|
||||
* @param {Object} jsonObj
|
||||
* @param {function} eventMapper
|
||||
* @return {SearchResult}
|
||||
*/
|
||||
|
||||
SearchResult.fromJson = function(jsonObj, eventMapper) {
|
||||
var jsonContext = jsonObj.context || {};
|
||||
var events_before = jsonContext.events_before || [];
|
||||
var events_after = jsonContext.events_after || [];
|
||||
|
||||
var context = new EventContext(eventMapper(jsonObj.result));
|
||||
|
||||
context.setPaginateToken(jsonContext.start, true);
|
||||
context.addEvents(utils.map(events_before, eventMapper), true);
|
||||
context.addEvents(utils.map(events_after, eventMapper), false);
|
||||
context.setPaginateToken(jsonContext.end, false);
|
||||
|
||||
return new SearchResult(jsonObj.rank, context);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The SearchResult class
|
||||
*/
|
||||
module.exports = SearchResult;
|
||||
@@ -1,199 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
/**
|
||||
* @module models/user
|
||||
*/
|
||||
var EventEmitter = require("events").EventEmitter;
|
||||
var utils = require("../utils");
|
||||
|
||||
/**
|
||||
* Construct a new User. A User must have an ID and can optionally have extra
|
||||
* information associated with it.
|
||||
* @constructor
|
||||
* @param {string} userId Required. The ID of this user.
|
||||
* @prop {string} userId The ID of the user.
|
||||
* @prop {Object} info The info object supplied in the constructor.
|
||||
* @prop {string} displayName The 'displayname' of the user if known.
|
||||
* @prop {string} avatarUrl The 'avatar_url' of the user if known.
|
||||
* @prop {string} presence The presence enum if known.
|
||||
* @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted
|
||||
* proactively with the server, or we saw a message from the user
|
||||
* @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last
|
||||
* received presence data for this user. We can subtract
|
||||
* lastActiveAgo from this to approximate an absolute value for
|
||||
* when a user was last active.
|
||||
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
|
||||
* an approximation and that the user should be seen as active 'now'
|
||||
* @prop {Object} events The events describing this user.
|
||||
* @prop {MatrixEvent} events.presence The m.presence event for this user.
|
||||
*/
|
||||
function User(userId) {
|
||||
this.userId = userId;
|
||||
this.presence = "offline";
|
||||
this.displayName = userId;
|
||||
this.avatarUrl = null;
|
||||
this.lastActiveAgo = 0;
|
||||
this.lastPresenceTs = 0;
|
||||
this.currentlyActive = false;
|
||||
this.events = {
|
||||
presence: null,
|
||||
profile: null
|
||||
};
|
||||
this._updateModifiedTime();
|
||||
}
|
||||
utils.inherits(User, EventEmitter);
|
||||
|
||||
/**
|
||||
* Update this User with the given presence event. May fire "User.presence",
|
||||
* "User.avatarUrl" and/or "User.displayName" if this event updates this user's
|
||||
* properties.
|
||||
* @param {MatrixEvent} event The <code>m.presence</code> event.
|
||||
* @fires module:client~MatrixClient#event:"User.presence"
|
||||
* @fires module:client~MatrixClient#event:"User.displayName"
|
||||
* @fires module:client~MatrixClient#event:"User.avatarUrl"
|
||||
*/
|
||||
User.prototype.setPresenceEvent = function(event) {
|
||||
if (event.getType() !== "m.presence") {
|
||||
return;
|
||||
}
|
||||
var firstFire = this.events.presence === null;
|
||||
this.events.presence = event;
|
||||
|
||||
var eventsToFire = [];
|
||||
if (event.getContent().presence !== this.presence || firstFire) {
|
||||
eventsToFire.push("User.presence");
|
||||
}
|
||||
if (event.getContent().avatar_url &&
|
||||
event.getContent().avatar_url !== this.avatarUrl)
|
||||
{
|
||||
eventsToFire.push("User.avatarUrl");
|
||||
}
|
||||
if (event.getContent().displayname &&
|
||||
event.getContent().displayname !== this.displayName)
|
||||
{
|
||||
eventsToFire.push("User.displayName");
|
||||
}
|
||||
|
||||
this.presence = event.getContent().presence;
|
||||
if (event.getContent().displayname) {
|
||||
this.displayName = event.getContent().displayname;
|
||||
}
|
||||
if (event.getContent().avatar_url) {
|
||||
this.avatarUrl = event.getContent().avatar_url;
|
||||
}
|
||||
this.lastActiveAgo = event.getContent().last_active_ago;
|
||||
this.lastPresenceTs = Date.now();
|
||||
this.currentlyActive = event.getContent().currently_active;
|
||||
|
||||
if (eventsToFire.length > 0) {
|
||||
this._updateModifiedTime();
|
||||
}
|
||||
|
||||
for (var i = 0; i < eventsToFire.length; i++) {
|
||||
this.emit(eventsToFire[i], event, this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Manually set this user's display name. No event is emitted in response to this
|
||||
* as there is no underlying MatrixEvent to emit with.
|
||||
* @param {string} name The new display name.
|
||||
*/
|
||||
User.prototype.setDisplayName = function(name) {
|
||||
var oldName = this.displayName;
|
||||
this.displayName = name;
|
||||
if (name !== oldName) {
|
||||
this._updateModifiedTime();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Manually set this user's avatar URL. No event is emitted in response to this
|
||||
* as there is no underlying MatrixEvent to emit with.
|
||||
* @param {string} url The new avatar URL.
|
||||
*/
|
||||
User.prototype.setAvatarUrl = function(url) {
|
||||
var oldUrl = this.avatarUrl;
|
||||
this.avatarUrl = url;
|
||||
if (url !== oldUrl) {
|
||||
this._updateModifiedTime();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the last modified time to the current time.
|
||||
*/
|
||||
User.prototype._updateModifiedTime = function() {
|
||||
this._modified = Date.now();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the timestamp when this User was last updated. This timestamp is
|
||||
* updated when this User receives a new Presence event which has updated a
|
||||
* property on this object. It is updated <i>before</i> firing events.
|
||||
* @return {number} The timestamp
|
||||
*/
|
||||
User.prototype.getLastModifiedTime = function() {
|
||||
return this._modified;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the absolute timestamp when this User was last known active on the server.
|
||||
* It is *NOT* accurate if this.currentlyActive is true.
|
||||
* @return {number} The timestamp
|
||||
*/
|
||||
User.prototype.getLastActiveTs = function() {
|
||||
return this.lastPresenceTs - this.lastActiveAgo;
|
||||
};
|
||||
|
||||
/**
|
||||
* The User class.
|
||||
*/
|
||||
module.exports = User;
|
||||
|
||||
/**
|
||||
* Fires whenever any user's presence changes.
|
||||
* @event module:client~MatrixClient#"User.presence"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {User} user The user whose User.presence changed.
|
||||
* @example
|
||||
* matrixClient.on("User.presence", function(event, user){
|
||||
* var newPresence = user.presence;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever any user's display name changes.
|
||||
* @event module:client~MatrixClient#"User.displayName"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {User} user The user whose User.displayName changed.
|
||||
* @example
|
||||
* matrixClient.on("User.displayName", function(event, user){
|
||||
* var newName = user.displayName;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever any user's avatar URL changes.
|
||||
* @event module:client~MatrixClient#"User.avatarUrl"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {User} user The user whose User.avatarUrl changed.
|
||||
* @example
|
||||
* matrixClient.on("User.avatarUrl", function(event, user){
|
||||
* var newUrl = user.avatarUrl;
|
||||
* });
|
||||
*/
|
||||
@@ -1,278 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
/**
|
||||
* @module pushprocessor
|
||||
*/
|
||||
|
||||
/**
|
||||
* Construct a Push Processor.
|
||||
* @constructor
|
||||
* @param {Object} client The Matrix client object to use
|
||||
*/
|
||||
function PushProcessor(client) {
|
||||
var escapeRegExp = function(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
};
|
||||
|
||||
var matchingRuleFromKindSet = function(ev, kindset, device) {
|
||||
var rulekinds_in_order = ['override', 'content', 'room', 'sender', 'underride'];
|
||||
for (var ruleKindIndex = 0;
|
||||
ruleKindIndex < rulekinds_in_order.length;
|
||||
++ruleKindIndex) {
|
||||
var kind = rulekinds_in_order[ruleKindIndex];
|
||||
var ruleset = kindset[kind];
|
||||
|
||||
for (var ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
|
||||
var rule = ruleset[ruleIndex];
|
||||
if (!rule.enabled) { continue; }
|
||||
|
||||
var rawrule = templateRuleToRaw(kind, rule, device);
|
||||
if (!rawrule) { continue; }
|
||||
|
||||
if (ruleMatchesEvent(rawrule, ev)) {
|
||||
rule.kind = kind;
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
var templateRuleToRaw = function(kind, tprule, device) {
|
||||
var rawrule = {
|
||||
'rule_id': tprule.rule_id,
|
||||
'actions': tprule.actions,
|
||||
'conditions': []
|
||||
};
|
||||
switch (kind) {
|
||||
case 'underride':
|
||||
case 'override':
|
||||
rawrule.conditions = tprule.conditions;
|
||||
break;
|
||||
case 'room':
|
||||
if (!tprule.rule_id) { return null; }
|
||||
rawrule.conditions.push({
|
||||
'kind': 'event_match',
|
||||
'key': 'room_id',
|
||||
'pattern': tprule.rule_id
|
||||
});
|
||||
break;
|
||||
case 'sender':
|
||||
if (!tprule.rule_id) { return null; }
|
||||
rawrule.conditions.push({
|
||||
'kind': 'event_match',
|
||||
'key': 'user_id',
|
||||
'pattern': tprule.rule_id
|
||||
});
|
||||
break;
|
||||
case 'content':
|
||||
if (!tprule.pattern) { return null; }
|
||||
rawrule.conditions.push({
|
||||
'kind': 'event_match',
|
||||
'key': 'content.body',
|
||||
'pattern': tprule.pattern
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (device) {
|
||||
rawrule.conditions.push({
|
||||
'kind': 'device',
|
||||
'profile_tag': device
|
||||
});
|
||||
}
|
||||
return rawrule;
|
||||
};
|
||||
|
||||
var ruleMatchesEvent = function(rule, ev) {
|
||||
var ret = true;
|
||||
for (var i = 0; i < rule.conditions.length; ++i) {
|
||||
var cond = rule.conditions[i];
|
||||
ret &= eventFulfillsCondition(cond, ev);
|
||||
}
|
||||
//console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match"));
|
||||
return ret;
|
||||
};
|
||||
|
||||
var eventFulfillsCondition = function(cond, ev) {
|
||||
var condition_functions = {
|
||||
"event_match": eventFulfillsEventMatchCondition,
|
||||
"device": eventFulfillsDeviceCondition,
|
||||
"contains_display_name": eventFulfillsDisplayNameCondition,
|
||||
"room_member_count": eventFulfillsRoomMemberCountCondition
|
||||
};
|
||||
if (condition_functions[cond.kind]) {
|
||||
return condition_functions[cond.kind](cond, ev);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
var eventFulfillsRoomMemberCountCondition = function(cond, ev) {
|
||||
if (!cond.is) { return false; }
|
||||
|
||||
var room = client.getRoom(ev.room_id);
|
||||
if (!room || !room.currentState || !room.currentState.members) { return false; }
|
||||
|
||||
var memberCount = Object.keys(room.currentState.members).filter(function(m) {
|
||||
return room.currentState.members[m].membership == 'join';
|
||||
}).length;
|
||||
|
||||
var m = cond.is.match(/^([=<>]*)([0-9]*)$/);
|
||||
if (!m) { return false; }
|
||||
var ineq = m[1];
|
||||
var rhs = parseInt(m[2]);
|
||||
if (isNaN(rhs)) { return false; }
|
||||
switch (ineq) {
|
||||
case '':
|
||||
case '==':
|
||||
return memberCount == rhs;
|
||||
case '<':
|
||||
return memberCount < rhs;
|
||||
case '>':
|
||||
return memberCount > rhs;
|
||||
case '<=':
|
||||
return memberCount <= rhs;
|
||||
case '>=':
|
||||
return memberCount >= rhs;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
var eventFulfillsDisplayNameCondition = function(cond, ev) {
|
||||
if (!ev.content || ! ev.content.body || typeof ev.content.body != 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
var room = client.getRoom(ev.room_id);
|
||||
if (!room || !room.currentState || !room.currentState.members ||
|
||||
!room.currentState.getMember(client.credentials.userId)) { return false; }
|
||||
|
||||
var displayName = room.currentState.getMember(client.credentials.userId).name;
|
||||
|
||||
// N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
|
||||
// as shorthand for [^0-9A-Za-z_].
|
||||
var pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
|
||||
return ev.content.body.search(pat) > -1;
|
||||
};
|
||||
|
||||
var eventFulfillsDeviceCondition = function(cond, ev) {
|
||||
return false; // XXX: Allow a profile tag to be set for the web client instance
|
||||
};
|
||||
|
||||
var eventFulfillsEventMatchCondition = function(cond, ev) {
|
||||
var val = valueForDottedKey(cond.key, ev);
|
||||
if (!val || typeof val != 'string') { return false; }
|
||||
|
||||
var pat;
|
||||
if (cond.key == 'content.body') {
|
||||
pat = '(^|\\W)' + globToRegexp(cond.pattern) + '(\\W|$)';
|
||||
} else {
|
||||
pat = '^' + globToRegexp(cond.pattern) + '$';
|
||||
}
|
||||
var regex = new RegExp(pat, 'i');
|
||||
return !!val.match(regex);
|
||||
};
|
||||
|
||||
var globToRegexp = function(glob) {
|
||||
// From
|
||||
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
|
||||
// Because micromatch is about 130KB with dependencies,
|
||||
// and minimatch is not much better.
|
||||
var pat = escapeRegExp(glob);
|
||||
pat = pat.replace(/\\\*/, '.*');
|
||||
pat = pat.replace(/\?/, '.');
|
||||
pat = pat.replace(/\\\[(!|)(.*)\\]/, function(match, p1, p2, offset, string) {
|
||||
var first = p1 && '^' || '';
|
||||
var second = p2.replace(/\\\-/, '-');
|
||||
return '[' + first + second + ']';
|
||||
});
|
||||
return pat;
|
||||
};
|
||||
|
||||
var valueForDottedKey = function(key, ev) {
|
||||
var parts = key.split('.');
|
||||
var val = ev;
|
||||
while (parts.length > 0) {
|
||||
var thispart = parts.shift();
|
||||
if (!val[thispart]) { return null; }
|
||||
val = val[thispart];
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
var matchingRuleForEventWithRulesets = function(ev, rulesets) {
|
||||
if (!rulesets || !rulesets.device) { return null; }
|
||||
if (ev.user_id == client.credentials.userId) { return null; }
|
||||
|
||||
var allDevNames = Object.keys(rulesets.device);
|
||||
for (var i = 0; i < allDevNames.length; ++i) {
|
||||
var devname = allDevNames[i];
|
||||
var devrules = rulesets.device[devname];
|
||||
|
||||
var matchingRule = matchingRuleFromKindSet(devrules, devname);
|
||||
if (matchingRule) { return matchingRule; }
|
||||
}
|
||||
return matchingRuleFromKindSet(ev, rulesets.global);
|
||||
};
|
||||
|
||||
var actionListToActionsObject = function(actionlist) {
|
||||
var actionobj = { 'notify': false, 'tweaks': {} };
|
||||
for (var i = 0; i < actionlist.length; ++i) {
|
||||
var action = actionlist[i];
|
||||
if (action === 'notify') {
|
||||
actionobj.notify = true;
|
||||
} else if (typeof action === 'object') {
|
||||
if (action.value === undefined) { action.value = true; }
|
||||
actionobj.tweaks[action.set_tweak] = action.value;
|
||||
}
|
||||
}
|
||||
return actionobj;
|
||||
};
|
||||
|
||||
var pushActionsForEventAndRulesets = function(ev, rulesets) {
|
||||
var rule = matchingRuleForEventWithRulesets(ev, rulesets);
|
||||
if (!rule) { return {}; }
|
||||
|
||||
var actionObj = actionListToActionsObject(rule.actions);
|
||||
|
||||
// Some actions are implicit in some situations: we add those here
|
||||
if (actionObj.tweaks.highlight === undefined) {
|
||||
// if it isn't specified, highlight if it's a content
|
||||
// rule but otherwise not
|
||||
actionObj.tweaks.highlight = (rule.kind == 'content');
|
||||
}
|
||||
|
||||
return actionObj;
|
||||
};
|
||||
|
||||
this.actionsForEvent = function(ev) {
|
||||
return pushActionsForEventAndRulesets(ev, client.pushRules);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} PushAction
|
||||
* @type {Object}
|
||||
* @property {boolean} notify Whether this event should notify the user or not.
|
||||
* @property {Object} tweaks How this event should be notified.
|
||||
* @property {boolean} tweaks.highlight Whether this event should be highlighted
|
||||
* on the UI.
|
||||
* @property {boolean} tweaks.sound Whether this notification should produce a
|
||||
* noise.
|
||||
*/
|
||||
|
||||
/** The PushProcessor class. */
|
||||
module.exports = PushProcessor;
|
||||
@@ -1,315 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
/**
|
||||
* This is an internal module which manages queuing, scheduling and retrying
|
||||
* of requests.
|
||||
* @module scheduler
|
||||
*/
|
||||
var utils = require("./utils");
|
||||
var q = require("q");
|
||||
|
||||
var DEBUG = false; // set true to enable console logging.
|
||||
|
||||
/**
|
||||
* Construct a scheduler for Matrix. Requires
|
||||
* {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided
|
||||
* with a way of processing events.
|
||||
* @constructor
|
||||
* @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry
|
||||
* algorithm to apply when determining when to try to send an event again.
|
||||
* Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}.
|
||||
* @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing
|
||||
* algorithm to apply when determining which events should be sent before the
|
||||
* given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}.
|
||||
*/
|
||||
function MatrixScheduler(retryAlgorithm, queueAlgorithm) {
|
||||
this.retryAlgorithm = retryAlgorithm || MatrixScheduler.RETRY_BACKOFF_RATELIMIT;
|
||||
this.queueAlgorithm = queueAlgorithm || MatrixScheduler.QUEUE_MESSAGES;
|
||||
this._queues = {
|
||||
// queueName: [{
|
||||
// event: MatrixEvent, // event to send
|
||||
// defer: Deferred, // defer to resolve/reject at the END of the retries
|
||||
// attempts: Number // number of times we've called processFn
|
||||
// }, ...]
|
||||
};
|
||||
this._activeQueues = [];
|
||||
this._procFn = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a queue based on an event. The event provided does not need to be in
|
||||
* the queue.
|
||||
* @param {MatrixEvent} event An event to get the queue for.
|
||||
* @return {?Array<MatrixEvent>} A shallow copy of events in the queue or null.
|
||||
* Modifying this array will not modify the list itself. Modifying events in
|
||||
* this array <i>will</i> modify the underlying event in the queue.
|
||||
* @see MatrixScheduler.removeEventFromQueue To remove an event from the queue.
|
||||
*/
|
||||
MatrixScheduler.prototype.getQueueForEvent = function(event) {
|
||||
var name = this.queueAlgorithm(event);
|
||||
if (!name || !this._queues[name]) {
|
||||
return null;
|
||||
}
|
||||
return utils.map(this._queues[name], function(obj) {
|
||||
return obj.event;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove this event from the queue. The event is equal to another event if they
|
||||
* have the same ID returned from event.getId().
|
||||
* @param {MatrixEvent} event The event to remove.
|
||||
* @return {boolean} True if this event was removed.
|
||||
*/
|
||||
MatrixScheduler.prototype.removeEventFromQueue = function(event) {
|
||||
var name = this.queueAlgorithm(event);
|
||||
if (!name || !this._queues[name]) {
|
||||
return false;
|
||||
}
|
||||
var removed = false;
|
||||
utils.removeElement(this._queues[name], function(element) {
|
||||
if (element.event.getId() === event.getId()) {
|
||||
removed = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return removed;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the process function. Required for events in the queue to be processed.
|
||||
* If set after events have been added to the queue, this will immediately start
|
||||
* processing them.
|
||||
* @param {module:scheduler~processFn} fn The function that can process events
|
||||
* in the queue.
|
||||
*/
|
||||
MatrixScheduler.prototype.setProcessFunction = function(fn) {
|
||||
this._procFn = fn;
|
||||
_startProcessingQueues(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Queue an event if it is required and start processing queues.
|
||||
* @param {MatrixEvent} event The event that may be queued.
|
||||
* @return {?Promise} A promise if the event was queued, which will be
|
||||
* resolved or rejected in due time, else null.
|
||||
*/
|
||||
MatrixScheduler.prototype.queueEvent = function(event) {
|
||||
var queueName = this.queueAlgorithm(event);
|
||||
if (!queueName) {
|
||||
return null;
|
||||
}
|
||||
// add the event to the queue and make a deferred for it.
|
||||
if (!this._queues[queueName]) {
|
||||
this._queues[queueName] = [];
|
||||
}
|
||||
var defer = q.defer();
|
||||
this._queues[queueName].push({
|
||||
event: event,
|
||||
defer: defer,
|
||||
attempts: 0
|
||||
});
|
||||
debuglog(
|
||||
"Queue algorithm dumped event %s into queue '%s'",
|
||||
event.getId(), queueName
|
||||
);
|
||||
_startProcessingQueues(this);
|
||||
return defer.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retries events up to 4 times using exponential backoff. This produces wait
|
||||
* times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the
|
||||
* failure was due to a rate limited request, the time specified in the error is
|
||||
* waited before being retried.
|
||||
* @param {MatrixEvent} event
|
||||
* @param {Number} attempts
|
||||
* @param {MatrixError} err
|
||||
* @return {Number}
|
||||
* @see module:scheduler~retryAlgorithm
|
||||
*/
|
||||
MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
|
||||
if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) {
|
||||
// client error; no amount of retrying with save you now.
|
||||
return -1;
|
||||
}
|
||||
// we ship with browser-request which returns { cors: rejected } when trying
|
||||
// with no connection, so if we match that, give up since they have no conn.
|
||||
if (err.cors === "rejected") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (err.name === "M_LIMIT_EXCEEDED") {
|
||||
var waitTime = err.data.retry_after_ms;
|
||||
if (waitTime) {
|
||||
return waitTime;
|
||||
}
|
||||
}
|
||||
if (attempts > 4) {
|
||||
return -1; // give up
|
||||
}
|
||||
return (1000 * Math.pow(2, attempts));
|
||||
};
|
||||
|
||||
/**
|
||||
* Queues <code>m.room.message</code> events and lets other events continue
|
||||
* concurrently.
|
||||
* @param {MatrixEvent} event
|
||||
* @return {string}
|
||||
* @see module:scheduler~queueAlgorithm
|
||||
*/
|
||||
MatrixScheduler.QUEUE_MESSAGES = function(event) {
|
||||
if (event.getType() === "m.room.message") {
|
||||
// put these events in the 'message' queue.
|
||||
return "message";
|
||||
}
|
||||
// allow all other events continue concurrently.
|
||||
return null;
|
||||
};
|
||||
|
||||
function _startProcessingQueues(scheduler) {
|
||||
if (!scheduler._procFn) {
|
||||
return;
|
||||
}
|
||||
// for each inactive queue with events in them
|
||||
utils.forEach(utils.filter(utils.keys(scheduler._queues), function(queueName) {
|
||||
return scheduler._activeQueues.indexOf(queueName) === -1 &&
|
||||
scheduler._queues[queueName].length > 0;
|
||||
}), function(queueName) {
|
||||
// mark the queue as active
|
||||
scheduler._activeQueues.push(queueName);
|
||||
// begin processing the head of the queue
|
||||
debuglog("Spinning up queue: '%s'", queueName);
|
||||
_processQueue(scheduler, queueName);
|
||||
});
|
||||
}
|
||||
|
||||
function _processQueue(scheduler, queueName) {
|
||||
// get head of queue
|
||||
var obj = _peekNextEvent(scheduler, queueName);
|
||||
if (!obj) {
|
||||
// queue is empty. Mark as inactive and stop recursing.
|
||||
var index = scheduler._activeQueues.indexOf(queueName);
|
||||
if (index >= 0) {
|
||||
scheduler._activeQueues.splice(index, 1);
|
||||
}
|
||||
debuglog("Stopping queue '%s' as it is now empty", queueName);
|
||||
return;
|
||||
}
|
||||
debuglog(
|
||||
"Queue '%s' has %s pending events",
|
||||
queueName, scheduler._queues[queueName].length
|
||||
);
|
||||
// fire the process function and if it resolves, resolve the deferred. Else
|
||||
// invoke the retry algorithm.
|
||||
scheduler._procFn(obj.event).done(function(res) {
|
||||
// remove this from the queue
|
||||
_removeNextEvent(scheduler, queueName);
|
||||
debuglog("Queue '%s' sent event %s", queueName, obj.event.getId());
|
||||
obj.defer.resolve(res);
|
||||
// keep processing
|
||||
_processQueue(scheduler, queueName);
|
||||
}, function(err) {
|
||||
obj.attempts += 1;
|
||||
// ask the retry algorithm when/if we should try again
|
||||
var waitTimeMs = scheduler.retryAlgorithm(obj.event, obj.attempts, err);
|
||||
debuglog(
|
||||
"retry(%s) err=%s event_id=%s waitTime=%s",
|
||||
obj.attempts, err, obj.event.getId(), waitTimeMs
|
||||
);
|
||||
if (waitTimeMs === -1) { // give up (you quitter!)
|
||||
debuglog(
|
||||
"Queue '%s' giving up on event %s", queueName, obj.event.getId()
|
||||
);
|
||||
// remove this from the queue
|
||||
_removeNextEvent(scheduler, queueName);
|
||||
obj.defer.reject(err);
|
||||
// process next event
|
||||
_processQueue(scheduler, queueName);
|
||||
}
|
||||
else {
|
||||
setTimeout(function() {
|
||||
_processQueue(scheduler, queueName);
|
||||
}, waitTimeMs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _peekNextEvent(scheduler, queueName) {
|
||||
var queue = scheduler._queues[queueName];
|
||||
if (!utils.isArray(queue)) {
|
||||
return null;
|
||||
}
|
||||
return queue[0];
|
||||
}
|
||||
|
||||
function _removeNextEvent(scheduler, queueName) {
|
||||
var queue = scheduler._queues[queueName];
|
||||
if (!utils.isArray(queue)) {
|
||||
return null;
|
||||
}
|
||||
return queue.shift();
|
||||
}
|
||||
|
||||
function debuglog() {
|
||||
if (DEBUG) {
|
||||
console.log.apply(console, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The retry algorithm to apply when retrying events. To stop retrying, return
|
||||
* <code>-1</code>. If this event was part of a queue, it will be removed from
|
||||
* the queue.
|
||||
* @callback retryAlgorithm
|
||||
* @param {MatrixEvent} event The event being retried.
|
||||
* @param {Number} attempts The number of failed attempts. This will always be
|
||||
* >= 1.
|
||||
* @param {MatrixError} err The most recent error message received when trying
|
||||
* to send this event.
|
||||
* @return {Number} The number of milliseconds to wait before trying again. If
|
||||
* this is 0, the request will be immediately retried. If this is
|
||||
* <code>-1</code>, the event will be marked as
|
||||
* {@link module:models/event.EventStatus.NOT_SENT} and will not be retried.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The queuing algorithm to apply to events. This function must be idempotent as
|
||||
* it may be called multiple times with the same event. All queues created are
|
||||
* serviced in a FIFO manner. To send the event ASAP, return <code>null</code>
|
||||
* which will not put this event in a queue. Events that fail to send that form
|
||||
* part of a queue will be removed from the queue and the next event in the
|
||||
* queue will be sent.
|
||||
* @callback queueAlgorithm
|
||||
* @param {MatrixEvent} event The event to be sent.
|
||||
* @return {string} The name of the queue to put the event into. If a queue with
|
||||
* this name does not exist, it will be created. If this is <code>null</code>,
|
||||
* the event is not put into a queue and will be sent concurrently.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The function to invoke to process (send) events in the queue.
|
||||
* @callback processFn
|
||||
* @param {MatrixEvent} event The event to send.
|
||||
* @return {Promise} Resolved/rejected depending on the outcome of the request.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The MatrixScheduler class.
|
||||
*/
|
||||
module.exports = MatrixScheduler;
|
||||
@@ -1,254 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
/**
|
||||
* This is an internal module. See {@link MatrixInMemoryStore} for the public class.
|
||||
* @module store/memory
|
||||
*/
|
||||
var utils = require("../utils");
|
||||
var User = require("../models/user");
|
||||
|
||||
/**
|
||||
* Construct a new in-memory data store for the Matrix Client.
|
||||
* @constructor
|
||||
* @param {Object=} opts Config options
|
||||
* @param {LocalStorage} opts.localStorage The local storage instance to persist
|
||||
* some forms of data such as tokens. Rooms will NOT be stored. See
|
||||
* {@link WebStorageStore} to persist rooms.
|
||||
*/
|
||||
module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
|
||||
opts = opts || {};
|
||||
this.rooms = {
|
||||
// roomId: Room
|
||||
};
|
||||
this.users = {
|
||||
// userId: User
|
||||
};
|
||||
this.syncToken = null;
|
||||
this.filters = {
|
||||
// userId: {
|
||||
// filterId: Filter
|
||||
// }
|
||||
};
|
||||
this.localStorage = opts.localStorage;
|
||||
};
|
||||
|
||||
module.exports.MatrixInMemoryStore.prototype = {
|
||||
|
||||
/**
|
||||
* Retrieve the token to stream from.
|
||||
* @return {string} The token or null.
|
||||
*/
|
||||
getSyncToken: function() {
|
||||
return this.syncToken;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Set the token to stream from.
|
||||
* @param {string} token The token to stream from.
|
||||
*/
|
||||
setSyncToken: function(token) {
|
||||
this.syncToken = token;
|
||||
},
|
||||
|
||||
/**
|
||||
* Store the given room.
|
||||
* @param {Room} room The room to be stored. All properties must be stored.
|
||||
*/
|
||||
storeRoom: function(room) {
|
||||
this.rooms[room.roomId] = room;
|
||||
// add listeners for room member changes so we can keep the room member
|
||||
// map up-to-date.
|
||||
room.currentState.on("RoomState.members", this._onRoomMember.bind(this));
|
||||
// add existing members
|
||||
var self = this;
|
||||
room.currentState.getMembers().forEach(function(m) {
|
||||
self._onRoomMember(null, room.currentState, m);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when a room member in a room being tracked by this store has been
|
||||
* updated.
|
||||
* @param {MatrixEvent} event
|
||||
* @param {RoomState} state
|
||||
* @param {RoomMember} member
|
||||
*/
|
||||
_onRoomMember: function(event, state, member) {
|
||||
if (member.membership === "invite") {
|
||||
// We do NOT add invited members because people love to typo user IDs
|
||||
// which would then show up in these lists (!)
|
||||
return;
|
||||
}
|
||||
|
||||
var user = this.users[member.userId] || new User(member.userId);
|
||||
if (member.name) {
|
||||
user.setDisplayName(member.name);
|
||||
}
|
||||
if (member.events.member && member.events.member.getContent().avatar_url) {
|
||||
user.setAvatarUrl(member.events.member.getContent().avatar_url);
|
||||
}
|
||||
this.users[user.userId] = user;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a room by its' room ID.
|
||||
* @param {string} roomId The room ID.
|
||||
* @return {Room} The room or null.
|
||||
*/
|
||||
getRoom: function(roomId) {
|
||||
return this.rooms[roomId] || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve all known rooms.
|
||||
* @return {Room[]} A list of rooms, which may be empty.
|
||||
*/
|
||||
getRooms: function() {
|
||||
return utils.values(this.rooms);
|
||||
},
|
||||
|
||||
/**
|
||||
* Permanently delete a room.
|
||||
* @param {string} roomId
|
||||
*/
|
||||
removeRoom: function(roomId) {
|
||||
if (this.rooms[roomId]) {
|
||||
this.rooms[roomId].removeListener("RoomState.members", this._onRoomMember);
|
||||
}
|
||||
delete this.rooms[roomId];
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a summary of all the rooms.
|
||||
* @return {RoomSummary[]} A summary of each room.
|
||||
*/
|
||||
getRoomSummaries: function() {
|
||||
return utils.map(utils.values(this.rooms), function(room) {
|
||||
return room.summary;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Store a User.
|
||||
* @param {User} user The user to store.
|
||||
*/
|
||||
storeUser: function(user) {
|
||||
this.users[user.userId] = user;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a User by its' user ID.
|
||||
* @param {string} userId The user ID.
|
||||
* @return {User} The user or null.
|
||||
*/
|
||||
getUser: function(userId) {
|
||||
return this.users[userId] || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve all known users.
|
||||
* @return {User[]} A list of users, which may be empty.
|
||||
*/
|
||||
getUsers: function() {
|
||||
return utils.values(this.users);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve scrollback for this room.
|
||||
* @param {Room} room The matrix room
|
||||
* @param {integer} limit The max number of old events to retrieve.
|
||||
* @return {Array<Object>} An array of objects which will be at most 'limit'
|
||||
* length and at least 0. The objects are the raw event JSON.
|
||||
*/
|
||||
scrollback: function(room, limit) {
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Store events for a room. The events have already been added to the timeline
|
||||
* @param {Room} room The room to store events for.
|
||||
* @param {Array<MatrixEvent>} events The events to store.
|
||||
* @param {string} token The token associated with these events.
|
||||
* @param {boolean} toStart True if these are paginated results.
|
||||
*/
|
||||
storeEvents: function(room, events, token, toStart) {
|
||||
// no-op because they've already been added to the room instance.
|
||||
},
|
||||
|
||||
/**
|
||||
* Store a filter.
|
||||
* @param {Filter} filter
|
||||
*/
|
||||
storeFilter: function(filter) {
|
||||
if (!filter) { return; }
|
||||
if (!this.filters[filter.userId]) {
|
||||
this.filters[filter.userId] = {};
|
||||
}
|
||||
this.filters[filter.userId][filter.filterId] = filter;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a filter.
|
||||
* @param {string} userId
|
||||
* @param {string} filterId
|
||||
* @return {?Filter} A filter or null.
|
||||
*/
|
||||
getFilter: function(userId, filterId) {
|
||||
if (!this.filters[userId] || !this.filters[userId][filterId]) {
|
||||
return null;
|
||||
}
|
||||
return this.filters[userId][filterId];
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a filter ID with the given name.
|
||||
* @param {string} filterName The filter name.
|
||||
* @return {?string} The filter ID or null.
|
||||
*/
|
||||
getFilterIdByName: function(filterName) {
|
||||
if (!this.localStorage) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return this.localStorage.getItem("mxjssdk_memory_filter_" + filterName);
|
||||
}
|
||||
catch (e) {}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a filter name to ID mapping.
|
||||
* @param {string} filterName
|
||||
* @param {string} filterId
|
||||
*/
|
||||
setFilterIdByName: function(filterName, filterId) {
|
||||
if (!this.localStorage) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.localStorage.setItem("mxjssdk_memory_filter_" + filterName, filterId);
|
||||
}
|
||||
catch (e) {}
|
||||
}
|
||||
|
||||
// TODO
|
||||
//setMaxHistoryPerRoom: function(maxHistory) {},
|
||||
|
||||
// TODO
|
||||
//reapOldMessages: function() {},
|
||||
};
|
||||
@@ -1,162 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @module store/session/webstorage
|
||||
*/
|
||||
|
||||
var utils = require("../../utils");
|
||||
|
||||
var DEBUG = false; // set true to enable console logging.
|
||||
var E2E_PREFIX = "session.e2e.";
|
||||
|
||||
/**
|
||||
* Construct a web storage session store, capable of storing account keys,
|
||||
* session keys and access tokens.
|
||||
* @constructor
|
||||
* @param {WebStorage} webStore A web storage implementation, e.g.
|
||||
* 'window.localStorage' or 'window.sessionStorage' or a custom implementation.
|
||||
* @throws if the supplied 'store' does not meet the Storage interface of the
|
||||
* WebStorage API.
|
||||
*/
|
||||
function WebStorageSessionStore(webStore) {
|
||||
this.store = webStore;
|
||||
if (!utils.isFunction(webStore.getItem) ||
|
||||
!utils.isFunction(webStore.setItem) ||
|
||||
!utils.isFunction(webStore.removeItem)) {
|
||||
throw new Error(
|
||||
"Supplied webStore does not meet the WebStorage API interface"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WebStorageSessionStore.prototype = {
|
||||
|
||||
/**
|
||||
* Store the end to end account for the logged-in user.
|
||||
* @param {string} account Base64 encoded account.
|
||||
*/
|
||||
storeEndToEndAccount: function(account) {
|
||||
this.store.setItem(KEY_END_TO_END_ACCOUNT, account);
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the end to end account for the logged-in user.
|
||||
* @return {?string} Base64 encoded account.
|
||||
*/
|
||||
getEndToEndAccount: function() {
|
||||
return this.store.getItem(KEY_END_TO_END_ACCOUNT);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores the known devices for a user.
|
||||
* @param {string} userId The user's ID.
|
||||
* @param {object} devices A map from device ID to keys for the device.
|
||||
*/
|
||||
storeEndToEndDevicesForUser: function(userId, devices) {
|
||||
setJsonItem(this.store, keyEndToEndDevicesForUser(userId), devices);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the known devices for a user.
|
||||
* @param {string} userId The user's ID.
|
||||
* @return {object} A map from device ID to keys for the device.
|
||||
*/
|
||||
getEndToEndDevicesForUser: function(userId) {
|
||||
return getJsonItem(this.store, keyEndToEndDevicesForUser(userId));
|
||||
},
|
||||
|
||||
/**
|
||||
* Store a session between the logged-in user and another device
|
||||
* @param {string} deviceKey The public key of the other device.
|
||||
* @param {string} sessionId The ID for this end-to-end session.
|
||||
* @param {string} session Base64 encoded end-to-end session.
|
||||
*/
|
||||
storeEndToEndSession: function(deviceKey, sessionId, session) {
|
||||
var sessions = this.getEndToEndSessions(deviceKey) || {};
|
||||
sessions[sessionId] = session;
|
||||
setJsonItem(
|
||||
this.store, keyEndToEndSessions(deviceKey), sessions
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the end-to-end sessions between the logged-in user and another
|
||||
* device.
|
||||
* @param {string} deviceKey The public key of the other device.
|
||||
* @return {object} A map from sessionId to Base64 end-to-end session.
|
||||
*/
|
||||
getEndToEndSessions: function(deviceKey) {
|
||||
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
|
||||
},
|
||||
|
||||
/**
|
||||
* Store the end-to-end state for a room.
|
||||
* @param {string} roomId The room's ID.
|
||||
* @param {object} roomInfo The end-to-end info for the room.
|
||||
*/
|
||||
storeEndToEndRoom: function(roomId, roomInfo) {
|
||||
setJsonItem(this.store, keyEndToEndRoom(roomId), roomInfo);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the end-to-end state for a room
|
||||
* @param {string} roomId The room's ID.
|
||||
* @return {object} The end-to-end info for the room.
|
||||
*/
|
||||
getEndToEndRoom: function(roomId) {
|
||||
return getJsonItem(this.store, keyEndToEndRoom(roomId));
|
||||
}
|
||||
};
|
||||
|
||||
var KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
|
||||
function keyEndToEndDevicesForUser(userId) {
|
||||
return E2E_PREFIX + "devices/" + userId;
|
||||
}
|
||||
|
||||
function keyEndToEndSessions(deviceKey) {
|
||||
return E2E_PREFIX + "sessions/" + deviceKey;
|
||||
}
|
||||
|
||||
function keyEndToEndRoom(roomId) {
|
||||
return E2E_PREFIX + "rooms/" + roomId;
|
||||
}
|
||||
|
||||
function getJsonItem(store, key) {
|
||||
try {
|
||||
return JSON.parse(store.getItem(key));
|
||||
}
|
||||
catch (e) {
|
||||
debuglog("Failed to get key %s: %s", key, e);
|
||||
debuglog(e.stack);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setJsonItem(store, key, val) {
|
||||
store.setItem(key, JSON.stringify(val));
|
||||
}
|
||||
|
||||
function debuglog() {
|
||||
if (DEBUG) {
|
||||
console.log.apply(console, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/** */
|
||||
module.exports = WebStorageSessionStore;
|
||||
@@ -1,175 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
/**
|
||||
* This is an internal module.
|
||||
* @module store/stub
|
||||
*/
|
||||
|
||||
/**
|
||||
* Construct a stub store. This does no-ops on most store methods.
|
||||
* @constructor
|
||||
*/
|
||||
function StubStore() {
|
||||
this.fromToken = null;
|
||||
}
|
||||
|
||||
StubStore.prototype = {
|
||||
|
||||
/**
|
||||
* Get the sync token.
|
||||
* @return {string}
|
||||
*/
|
||||
getSyncToken: function() {
|
||||
return this.fromToken;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the sync token.
|
||||
* @param {string} token
|
||||
*/
|
||||
setSyncToken: function(token) {
|
||||
this.fromToken = token;
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @param {Room} room
|
||||
*/
|
||||
storeRoom: function(room) {
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @param {string} roomId
|
||||
* @return {null}
|
||||
*/
|
||||
getRoom: function(roomId) {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @return {Array} An empty array.
|
||||
*/
|
||||
getRooms: function() {
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Permanently delete a room.
|
||||
* @param {string} roomId
|
||||
*/
|
||||
removeRoom: function(roomId) {
|
||||
return;
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @return {Array} An empty array.
|
||||
*/
|
||||
getRoomSummaries: function() {
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @param {User} user
|
||||
*/
|
||||
storeUser: function(user) {
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @param {string} userId
|
||||
* @return {null}
|
||||
*/
|
||||
getUser: function(userId) {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @return {User[]}
|
||||
*/
|
||||
getUsers: function() {
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @param {Room} room
|
||||
* @param {integer} limit
|
||||
* @return {Array}
|
||||
*/
|
||||
scrollback: function(room, limit) {
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Store events for a room.
|
||||
* @param {Room} room The room to store events for.
|
||||
* @param {Array<MatrixEvent>} events The events to store.
|
||||
* @param {string} token The token associated with these events.
|
||||
* @param {boolean} toStart True if these are paginated results.
|
||||
*/
|
||||
storeEvents: function(room, events, token, toStart) {
|
||||
},
|
||||
|
||||
/**
|
||||
* Store a filter.
|
||||
* @param {Filter} filter
|
||||
*/
|
||||
storeFilter: function(filter) {
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a filter.
|
||||
* @param {string} userId
|
||||
* @param {string} filterId
|
||||
* @return {?Filter} A filter or null.
|
||||
*/
|
||||
getFilter: function(userId, filterId) {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a filter ID with the given name.
|
||||
* @param {string} filterName The filter name.
|
||||
* @return {?string} The filter ID or null.
|
||||
*/
|
||||
getFilterIdByName: function(filterName) {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a filter name to ID mapping.
|
||||
* @param {string} filterName
|
||||
* @param {string} filterId
|
||||
*/
|
||||
setFilterIdByName: function(filterName, filterId) {
|
||||
|
||||
}
|
||||
|
||||
// TODO
|
||||
//setMaxHistoryPerRoom: function(maxHistory) {},
|
||||
|
||||
// TODO
|
||||
//reapOldMessages: function() {},
|
||||
};
|
||||
|
||||
/** Stub Store class. */
|
||||
module.exports = StubStore;
|
||||
@@ -1,686 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
/**
|
||||
* This is an internal module. Implementation details:
|
||||
* <pre>
|
||||
* Room data is stored as follows:
|
||||
* room_$ROOMID_timeline_$INDEX : [ Event, Event, Event ]
|
||||
* room_$ROOMID_state : {
|
||||
* pagination_token: <oldState.paginationToken>,
|
||||
* events: {
|
||||
* <event_type>: { <state_key> : {JSON} }
|
||||
* }
|
||||
* }
|
||||
* User data is stored as follows:
|
||||
* user_$USERID : User
|
||||
* Sync token:
|
||||
* sync_token : $TOKEN
|
||||
*
|
||||
* Room Retrieval
|
||||
* --------------
|
||||
* Retrieving a room requires the $ROOMID which then pulls out the current state
|
||||
* from room_$ROOMID_state. A defined starting batch of timeline events are then
|
||||
* extracted from the highest numbered $INDEX for room_$ROOMID_timeline_$INDEX
|
||||
* (more indices as required). The $INDEX may be negative. These are
|
||||
* added to the timeline in the same way as /initialSync (old state will diverge).
|
||||
* If there exists a room_$ROOMID_timeline_live key, then a timeline sync should
|
||||
* be performed before retrieving.
|
||||
*
|
||||
* Retrieval of earlier messages
|
||||
* -----------------------------
|
||||
* The earliest event the Room instance knows about is E. Retrieving earlier
|
||||
* messages requires a Room which has a storageToken defined.
|
||||
* This token maps to the index I where the Room is at. Events are then retrieved from
|
||||
* room_$ROOMID_timeline_{I} and elements before E are extracted. If the limit
|
||||
* demands more events, I-1 is retrieved, up until I=min $INDEX where it gives
|
||||
* less than the limit. Index may go negative if you have paginated in the past.
|
||||
*
|
||||
* Full Insertion
|
||||
* --------------
|
||||
* Storing a room requires the timeline and state keys for $ROOMID to
|
||||
* be blown away and completely replaced, which is computationally expensive.
|
||||
* Room.timeline is batched according to the given batch size B. These batches
|
||||
* are then inserted into storage as room_$ROOMID_timeline_$INDEX. Finally,
|
||||
* the current room state is persisted to room_$ROOMID_state.
|
||||
*
|
||||
* Incremental Insertion
|
||||
* ---------------------
|
||||
* As events arrive, the store can quickly persist these new events. This
|
||||
* involves pushing the events to room_$ROOMID_timeline_live. If the
|
||||
* current room state has been modified by the new event, then
|
||||
* room_$ROOMID_state should be updated in addition to the timeline.
|
||||
*
|
||||
* Timeline sync
|
||||
* -------------
|
||||
* Retrieval of events from the timeline depends on the proper batching of
|
||||
* events. This is computationally expensive to perform on every new event, so
|
||||
* is deferred by inserting live events to room_$ROOMID_timeline_live. A
|
||||
* timeline sync reconciles timeline_live and timeline_$INDEX. This involves
|
||||
* retrieving _live and the highest numbered $INDEX batch. If the batch is < B,
|
||||
* the earliest entries from _live are inserted into the $INDEX until the
|
||||
* batch == B. Then, the remaining entries in _live are batched to $INDEX+1,
|
||||
* $INDEX+2, and so on. The easiest way to visualise this is that the timeline
|
||||
* goes from old to new, left to right:
|
||||
* -2 -1 0 1
|
||||
* <--OLD---------------------------------------NEW-->
|
||||
* [a,b,c] [d,e,f] [g,h,i] [j,k,l]
|
||||
*
|
||||
* Purging
|
||||
* -------
|
||||
* Events from the timeline can be purged by removing the lowest
|
||||
* timeline_$INDEX in the store.
|
||||
*
|
||||
* Example
|
||||
* -------
|
||||
* A room with room_id !foo:bar has 9 messages (M1->9 where 9=newest) with a
|
||||
* batch size of 4. The very first time, there is no entry for !foo:bar until
|
||||
* storeRoom() is called, which results in the keys: [Full Insert]
|
||||
* room_!foo:bar_timeline_0 : [M1, M2, M3, M4]
|
||||
* room_!foo:bar_timeline_1 : [M5, M6, M7, M8]
|
||||
* room_!foo:bar_timeline_2 : [M9]
|
||||
* room_!foo:bar_state: { ... }
|
||||
*
|
||||
* 5 new messages (N1-5, 5=newest) arrive and are then added: [Incremental Insert]
|
||||
* room_!foo:bar_timeline_live: [N1]
|
||||
* room_!foo:bar_timeline_live: [N1, N2]
|
||||
* room_!foo:bar_timeline_live: [N1, N2, N3]
|
||||
* room_!foo:bar_timeline_live: [N1, N2, N3, N4]
|
||||
* room_!foo:bar_timeline_live: [N1, N2, N3, N4, N5]
|
||||
*
|
||||
* App is shutdown. Restarts. The timeline is synced [Timeline Sync]
|
||||
* room_!foo:bar_timeline_2 : [M9, N1, N2, N3]
|
||||
* room_!foo:bar_timeline_3 : [N4, N5]
|
||||
* room_!foo:bar_timeline_live: []
|
||||
*
|
||||
* And the room is retrieved with 8 messages: [Room Retrieval]
|
||||
* Room.timeline: [M7, M8, M9, N1, N2, N3, N4, N5]
|
||||
* Room.storageToken: => early_index = 1 because that's where M7 is.
|
||||
*
|
||||
* 3 earlier messages are requested: [Earlier retrieval]
|
||||
* Use storageToken to find batch index 1. Scan batch for earliest event ID.
|
||||
* earliest event = M7
|
||||
* events = room_!foo:bar_timeline_1 where event < M7 = [M5, M6]
|
||||
* Too few events, use next index (0) and get 1 more:
|
||||
* events = room_!foo:bar_timeline_0 = [M1, M2, M3, M4] => [M4]
|
||||
* Return concatentation:
|
||||
* [M4, M5, M6]
|
||||
*
|
||||
* Purge oldest events: [Purge]
|
||||
* del room_!foo:bar_timeline_0
|
||||
* </pre>
|
||||
* @module store/webstorage
|
||||
*/
|
||||
var DEBUG = false; // set true to enable console logging.
|
||||
var utils = require("../utils");
|
||||
var Room = require("../models/room");
|
||||
var User = require("../models/user");
|
||||
var MatrixEvent = require("../models/event").MatrixEvent;
|
||||
|
||||
/**
|
||||
* Construct a web storage store, capable of storing rooms and users.
|
||||
* @constructor
|
||||
* @param {WebStorage} webStore A web storage implementation, e.g.
|
||||
* 'window.localStorage' or 'window.sessionStorage' or a custom implementation.
|
||||
* @param {integer} batchSize The number of events to store per key/value (room
|
||||
* scoped). Use -1 to store all events for a room under one key/value.
|
||||
* @throws if the supplied 'store' does not meet the Storage interface of the
|
||||
* WebStorage API.
|
||||
*/
|
||||
function WebStorageStore(webStore, batchSize) {
|
||||
this.store = webStore;
|
||||
this.batchSize = batchSize;
|
||||
if (!utils.isFunction(webStore.getItem) || !utils.isFunction(webStore.setItem) ||
|
||||
!utils.isFunction(webStore.removeItem) || !utils.isFunction(webStore.key)) {
|
||||
throw new Error(
|
||||
"Supplied webStore does not meet the WebStorage API interface"
|
||||
);
|
||||
}
|
||||
if (!parseInt(webStore.length) && webStore.length !== 0) {
|
||||
throw new Error(
|
||||
"Supplied webStore does not meet the WebStorage API interface (length)"
|
||||
);
|
||||
}
|
||||
// cached list of room_ids this is storing.
|
||||
this._roomIds = [];
|
||||
this._syncedWithStore = false;
|
||||
// tokens used to remember which index the room instance is at.
|
||||
this._tokens = [
|
||||
// { earliestIndex: -4 }
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the token to stream from.
|
||||
* @return {string} The token or null.
|
||||
*/
|
||||
WebStorageStore.prototype.getSyncToken = function() {
|
||||
return this.store.getItem("sync_token");
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the token to stream from.
|
||||
* @param {string} token The token to stream from.
|
||||
*/
|
||||
WebStorageStore.prototype.setSyncToken = function(token) {
|
||||
this.store.setItem("sync_token", token);
|
||||
};
|
||||
|
||||
/**
|
||||
* Store a room in web storage.
|
||||
* @param {Room} room
|
||||
*/
|
||||
WebStorageStore.prototype.storeRoom = function(room) {
|
||||
var serRoom = SerialisedRoom.fromRoom(room, this.batchSize);
|
||||
persist(this.store, serRoom);
|
||||
if (this._roomIds.indexOf(room.roomId) === -1) {
|
||||
this._roomIds.push(room.roomId);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve a room from web storage.
|
||||
* @param {string} roomId
|
||||
* @return {?Room}
|
||||
*/
|
||||
WebStorageStore.prototype.getRoom = function(roomId) {
|
||||
// probe if room exists; break early if not. Every room should have state.
|
||||
if (!getItem(this.store, keyName(roomId, "state"))) {
|
||||
debuglog("getRoom: No room with id %s found.", roomId);
|
||||
return null;
|
||||
}
|
||||
var timelineKeys = getTimelineIndices(this.store, roomId);
|
||||
if (timelineKeys.indexOf("live") !== -1) {
|
||||
debuglog("getRoom: Live events found. Syncing timeline for %s", roomId);
|
||||
this._syncTimeline(roomId, timelineKeys);
|
||||
}
|
||||
return loadRoom(this.store, roomId, this.batchSize, this._tokens);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of all rooms from web storage.
|
||||
* @return {Array} An empty array.
|
||||
*/
|
||||
WebStorageStore.prototype.getRooms = function() {
|
||||
var rooms = [];
|
||||
var i;
|
||||
if (!this._syncedWithStore) {
|
||||
// sync with the store to set this._roomIds correctly. We know there is
|
||||
// exactly one 'state' key for each room, so we grab them.
|
||||
this._roomIds = [];
|
||||
for (i = 0; i < this.store.length; i++) {
|
||||
if (this.store.key(i).indexOf("room_") === 0 &&
|
||||
this.store.key(i).indexOf("_state") !== -1) {
|
||||
// grab the middle bit which is the room ID
|
||||
var k = this.store.key(i);
|
||||
this._roomIds.push(
|
||||
k.substring("room_".length, k.length - "_state".length)
|
||||
);
|
||||
}
|
||||
}
|
||||
this._syncedWithStore = true;
|
||||
}
|
||||
// call getRoom on each room_id
|
||||
for (i = 0; i < this._roomIds.length; i++) {
|
||||
var rm = this.getRoom(this._roomIds[i]);
|
||||
if (rm) {
|
||||
rooms.push(rm);
|
||||
}
|
||||
}
|
||||
return rooms;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of summaries from web storage.
|
||||
* @return {Array} An empty array.
|
||||
*/
|
||||
WebStorageStore.prototype.getRoomSummaries = function() {
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Store a user in web storage.
|
||||
* @param {User} user
|
||||
*/
|
||||
WebStorageStore.prototype.storeUser = function(user) {
|
||||
// persist the events used to make the user, we can reconstruct on demand.
|
||||
setItem(this.store, "user_" + user.userId, {
|
||||
presence: user.events.presence ? user.events.presence.event : null
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a user from web storage.
|
||||
* @param {string} userId
|
||||
* @return {User}
|
||||
*/
|
||||
WebStorageStore.prototype.getUser = function(userId) {
|
||||
var userData = getItem(this.store, "user_" + userId);
|
||||
if (!userData) {
|
||||
return null;
|
||||
}
|
||||
var user = new User(userId);
|
||||
if (userData.presence) {
|
||||
user.setPresenceEvent(new MatrixEvent(userData.presence));
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve scrollback for this room. Automatically adds events to the timeline.
|
||||
* @param {Room} room The matrix room to add the events to the start of the timeline.
|
||||
* @param {integer} limit The max number of old events to retrieve.
|
||||
* @return {Array<Object>} An array of objects which will be at most 'limit'
|
||||
* length and at least 0. The objects are the raw event JSON. The last element
|
||||
* is the 'oldest' (for parity with homeserver scrollback APIs).
|
||||
*/
|
||||
WebStorageStore.prototype.scrollback = function(room, limit) {
|
||||
if (room.storageToken === undefined || room.storageToken >= this._tokens.length) {
|
||||
return [];
|
||||
}
|
||||
// find the index of the earliest event in this room's timeline
|
||||
var storeData = this._tokens[room.storageToken] || {};
|
||||
var i;
|
||||
var earliestIndex = storeData.earliestIndex;
|
||||
var earliestEventId = room.timeline[0] ? room.timeline[0].getId() : null;
|
||||
debuglog(
|
||||
"scrollback in %s (timeline=%s msgs) i=%s, timeline[0].id=%s - req %s events",
|
||||
room.roomId, room.timeline.length, earliestIndex, earliestEventId, limit
|
||||
);
|
||||
var batch = getItem(
|
||||
this.store, keyName(room.roomId, "timeline", earliestIndex)
|
||||
);
|
||||
if (!batch) {
|
||||
// bad room or already at start, either way we have nothing to give.
|
||||
debuglog("No batch with index %s found.", earliestIndex);
|
||||
return [];
|
||||
}
|
||||
// populate from this batch first
|
||||
var scrollback = [];
|
||||
var foundEventId = false;
|
||||
for (i = batch.length - 1; i >= 0; i--) {
|
||||
// go back and find the earliest event ID, THEN start adding entries.
|
||||
// Make a MatrixEvent so we don't assume .event_id exists
|
||||
// (e.g v2/v3 JSON may be different)
|
||||
var matrixEvent = new MatrixEvent(batch[i]);
|
||||
if (matrixEvent.getId() === earliestEventId) {
|
||||
foundEventId = true;
|
||||
debuglog(
|
||||
"Found timeline[0] event at position %s in batch %s",
|
||||
i, earliestIndex
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!foundEventId) {
|
||||
continue;
|
||||
}
|
||||
// add entry
|
||||
debuglog("Add event at position %s in batch %s", i, earliestIndex);
|
||||
scrollback.push(batch[i]);
|
||||
if (scrollback.length === limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (scrollback.length === limit) {
|
||||
debuglog("Batch has enough events to satisfy request.");
|
||||
return scrollback;
|
||||
}
|
||||
if (!foundEventId) {
|
||||
// the earliest index batch didn't contain the event. In other words,
|
||||
// this timeline is at a state we don't know, so bail.
|
||||
debuglog(
|
||||
"Failed to find event ID %s in batch %s", earliestEventId, earliestIndex
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// get the requested earlier events from earlier batches
|
||||
while (scrollback.length < limit) {
|
||||
earliestIndex--;
|
||||
batch = getItem(
|
||||
this.store, keyName(room.roomId, "timeline", earliestIndex)
|
||||
);
|
||||
if (!batch) {
|
||||
// no more events
|
||||
debuglog("No batch found at index %s", earliestIndex);
|
||||
break;
|
||||
}
|
||||
for (i = batch.length - 1; i >= 0; i--) {
|
||||
debuglog("Add event at position %s in batch %s", i, earliestIndex);
|
||||
scrollback.push(batch[i]);
|
||||
if (scrollback.length === limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
debuglog(
|
||||
"Out of %s requested events, returning %s. New index=%s",
|
||||
limit, scrollback.length, earliestIndex
|
||||
);
|
||||
room.addEventsToTimeline(utils.map(scrollback, function(e) {
|
||||
return new MatrixEvent(e);
|
||||
}), true, room.getLiveTimeline());
|
||||
|
||||
this._tokens[room.storageToken] = {
|
||||
earliestIndex: earliestIndex
|
||||
};
|
||||
return scrollback;
|
||||
};
|
||||
|
||||
/**
|
||||
* Store events for a room. The events have already been added to the timeline.
|
||||
* @param {Room} room The room to store events for.
|
||||
* @param {Array<MatrixEvent>} events The events to store.
|
||||
* @param {string} token The token associated with these events.
|
||||
* @param {boolean} toStart True if these are paginated results. The last element
|
||||
* is the 'oldest' (for parity with homeserver scrollback APIs).
|
||||
*/
|
||||
WebStorageStore.prototype.storeEvents = function(room, events, token, toStart) {
|
||||
if (toStart) {
|
||||
// add paginated events to lowest batch indexes (can go -ve)
|
||||
var lowIndex = getIndexExtremity(
|
||||
getTimelineIndices(this.store, room.roomId), true
|
||||
);
|
||||
var i, key, batch;
|
||||
for (i = 0; i < events.length; i++) { // loop events to be stored
|
||||
key = keyName(room.roomId, "timeline", lowIndex);
|
||||
batch = getItem(this.store, key) || [];
|
||||
while (batch.length < this.batchSize && i < events.length) {
|
||||
batch.unshift(events[i].event);
|
||||
i++; // increment to insert next event into this batch
|
||||
}
|
||||
i--; // decrement to avoid skipping one (for loop ++s)
|
||||
setItem(this.store, key, batch);
|
||||
lowIndex--; // decrement index to get a new batch.
|
||||
}
|
||||
}
|
||||
else {
|
||||
// dump as live events
|
||||
var liveEvents = getItem(
|
||||
this.store, keyName(room.roomId, "timeline", "live")
|
||||
) || [];
|
||||
debuglog(
|
||||
"Adding %s events to %s live list (which has %s already)",
|
||||
events.length, room.roomId, liveEvents.length
|
||||
);
|
||||
var updateState = false;
|
||||
liveEvents = liveEvents.concat(utils.map(events, function(me) {
|
||||
// cheeky check to avoid looping twice
|
||||
if (me.isState()) {
|
||||
updateState = true;
|
||||
}
|
||||
return me.event;
|
||||
}));
|
||||
setItem(
|
||||
this.store, keyName(room.roomId, "timeline", "live"), liveEvents
|
||||
);
|
||||
if (updateState) {
|
||||
debuglog("Storing state for %s as new events updated state", room.roomId);
|
||||
// use 0 batch size; we don't care about batching right now.
|
||||
var serRoom = SerialisedRoom.fromRoom(room, 0);
|
||||
setItem(this.store, keyName(serRoom.roomId, "state"), serRoom.state);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync the 'live' timeline, batching live events according to 'batchSize'.
|
||||
* @param {string} roomId The room to sync the timeline.
|
||||
* @param {Array<String>} timelineIndices Optional. The indices in the timeline
|
||||
* if known already.
|
||||
*/
|
||||
WebStorageStore.prototype._syncTimeline = function(roomId, timelineIndices) {
|
||||
timelineIndices = timelineIndices || getTimelineIndices(this.store, roomId);
|
||||
var liveEvents = getItem(this.store, keyName(roomId, "timeline", "live")) || [];
|
||||
|
||||
// get the highest numbered $INDEX batch
|
||||
var highestIndex = getIndexExtremity(timelineIndices);
|
||||
var hiKey = keyName(roomId, "timeline", highestIndex);
|
||||
var hiBatch = getItem(this.store, hiKey) || [];
|
||||
// fill up the existing batch first.
|
||||
while (hiBatch.length < this.batchSize && liveEvents.length > 0) {
|
||||
hiBatch.push(liveEvents.shift());
|
||||
}
|
||||
setItem(this.store, hiKey, hiBatch);
|
||||
|
||||
// start adding new batches as required
|
||||
var batch = [];
|
||||
while (liveEvents.length > 0) {
|
||||
batch.push(liveEvents.shift());
|
||||
if (batch.length === this.batchSize || liveEvents.length === 0) {
|
||||
// persist the full batch and make another
|
||||
highestIndex++;
|
||||
hiKey = keyName(roomId, "timeline", highestIndex);
|
||||
setItem(this.store, hiKey, batch);
|
||||
batch = [];
|
||||
}
|
||||
}
|
||||
// reset live array
|
||||
setItem(this.store, keyName(roomId, "timeline", "live"), []);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Store a filter.
|
||||
* @param {Filter} filter
|
||||
*/
|
||||
WebStorageStore.prototype.storeFilter = function(filter) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve a filter.
|
||||
* @param {string} userId
|
||||
* @param {string} filterId
|
||||
* @return {?Filter} A filter or null.
|
||||
*/
|
||||
WebStorageStore.prototype.getFilter = function(userId, filterId) {
|
||||
return null;
|
||||
};
|
||||
|
||||
function SerialisedRoom(roomId) {
|
||||
this.state = {
|
||||
events: {}
|
||||
};
|
||||
this.timeline = {
|
||||
// $INDEX: []
|
||||
};
|
||||
this.roomId = roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Room instance into a SerialisedRoom instance which can be stored
|
||||
* in the key value store.
|
||||
* @param {Room} room The matrix room to convert
|
||||
* @param {integer} batchSize The number of events per timeline batch
|
||||
* @return {SerialisedRoom} A serialised room representation of 'room'.
|
||||
*/
|
||||
SerialisedRoom.fromRoom = function(room, batchSize) {
|
||||
var self = new SerialisedRoom(room.roomId);
|
||||
var index;
|
||||
self.state.pagination_token = room.oldState.paginationToken;
|
||||
// [room_$ROOMID_state] downcast to POJO from MatrixEvent
|
||||
utils.forEach(utils.keys(room.currentState.events), function(eventType) {
|
||||
utils.forEach(utils.keys(room.currentState.events[eventType]), function(skey) {
|
||||
if (!self.state.events[eventType]) {
|
||||
self.state.events[eventType] = {};
|
||||
}
|
||||
self.state.events[eventType][skey] = (
|
||||
room.currentState.events[eventType][skey].event
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// [room_$ROOMID_timeline_$INDEX]
|
||||
if (batchSize > 0) {
|
||||
index = 0;
|
||||
while (index * batchSize < room.timeline.length) {
|
||||
self.timeline[index] = room.timeline.slice(
|
||||
index * batchSize, (index + 1) * batchSize
|
||||
);
|
||||
self.timeline[index] = utils.map(self.timeline[index], function(me) {
|
||||
// use POJO not MatrixEvent
|
||||
return me.event;
|
||||
});
|
||||
index++;
|
||||
}
|
||||
}
|
||||
else { // don't batch
|
||||
self.timeline[0] = utils.map(room.timeline, function(matrixEvent) {
|
||||
return matrixEvent.event;
|
||||
});
|
||||
}
|
||||
return self;
|
||||
};
|
||||
|
||||
function loadRoom(store, roomId, numEvents, tokenArray) {
|
||||
var room = new Room(roomId, {
|
||||
storageToken: tokenArray.length
|
||||
});
|
||||
|
||||
// populate state (flatten nested struct to event array)
|
||||
var currentStateMap = getItem(store, keyName(roomId, "state"));
|
||||
var stateEvents = [];
|
||||
utils.forEach(utils.keys(currentStateMap.events), function(eventType) {
|
||||
utils.forEach(utils.keys(currentStateMap.events[eventType]), function(skey) {
|
||||
stateEvents.push(currentStateMap.events[eventType][skey]);
|
||||
});
|
||||
});
|
||||
// TODO: Fix logic dupe with MatrixClient._processRoomEvents
|
||||
var oldStateEvents = utils.map(
|
||||
utils.deepCopy(stateEvents), function(e) {
|
||||
return new MatrixEvent(e);
|
||||
}
|
||||
);
|
||||
var currentStateEvents = utils.map(stateEvents, function(e) {
|
||||
return new MatrixEvent(e);
|
||||
}
|
||||
);
|
||||
room.oldState.setStateEvents(oldStateEvents);
|
||||
room.currentState.setStateEvents(currentStateEvents);
|
||||
|
||||
// add most recent numEvents
|
||||
var recentEvents = [];
|
||||
var index = getIndexExtremity(getTimelineIndices(store, roomId));
|
||||
var eventIndex = index;
|
||||
var i, key, batch;
|
||||
while (recentEvents.length < numEvents) {
|
||||
key = keyName(roomId, "timeline", index);
|
||||
batch = getItem(store, key) || [];
|
||||
if (batch.length === 0) {
|
||||
// nothing left in the store.
|
||||
break;
|
||||
}
|
||||
for (i = batch.length - 1; i >= 0; i--) {
|
||||
recentEvents.unshift(new MatrixEvent(batch[i]));
|
||||
if (recentEvents.length === numEvents) {
|
||||
eventIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
index--;
|
||||
}
|
||||
// add events backwards to diverge old state correctly.
|
||||
room.addEventsToTimeline(recentEvents.reverse(), true, room.getLiveTimeline());
|
||||
room.oldState.paginationToken = currentStateMap.pagination_token;
|
||||
// set the token data to let us know which index this room instance is at
|
||||
// for scrollback.
|
||||
tokenArray.push({
|
||||
earliestIndex: eventIndex
|
||||
});
|
||||
return room;
|
||||
}
|
||||
|
||||
function persist(store, serRoom) {
|
||||
setItem(store, keyName(serRoom.roomId, "state"), serRoom.state);
|
||||
utils.forEach(utils.keys(serRoom.timeline), function(index) {
|
||||
setItem(store,
|
||||
keyName(serRoom.roomId, "timeline", index),
|
||||
serRoom.timeline[index]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getTimelineIndices(store, roomId) {
|
||||
var keys = [];
|
||||
for (var i = 0; i < store.length; i++) {
|
||||
if (store.key(i).indexOf(keyName(roomId, "timeline_")) !== -1) {
|
||||
// e.g. room_$ROOMID_timeline_0 => 0
|
||||
keys.push(
|
||||
store.key(i).replace(keyName(roomId, "timeline_"), "")
|
||||
);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function getIndexExtremity(timelineIndices, getLowest) {
|
||||
var extremity, index;
|
||||
for (var i = 0; i < timelineIndices.length; i++) {
|
||||
index = parseInt(timelineIndices[i]);
|
||||
if (!isNaN(index) && (
|
||||
extremity === undefined ||
|
||||
!getLowest && index > extremity ||
|
||||
getLowest && index < extremity)) {
|
||||
extremity = index;
|
||||
}
|
||||
}
|
||||
return extremity;
|
||||
}
|
||||
|
||||
function keyName(roomId, key, index) {
|
||||
return "room_" + roomId + "_" + key + (
|
||||
index === undefined ? "" : ("_" + index)
|
||||
);
|
||||
}
|
||||
|
||||
function getItem(store, key) {
|
||||
try {
|
||||
return JSON.parse(store.getItem(key));
|
||||
}
|
||||
catch (e) {
|
||||
debuglog("Failed to get key %s: %s", key, e);
|
||||
debuglog(e.stack);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setItem(store, key, val) {
|
||||
store.setItem(key, JSON.stringify(val));
|
||||
}
|
||||
|
||||
function debuglog() {
|
||||
if (DEBUG) {
|
||||
console.log.apply(console, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
function delRoomStruct(store, roomId) {
|
||||
var prefix = "room_" + roomId;
|
||||
var keysToRemove = [];
|
||||
for (var i = 0; i < store.length; i++) {
|
||||
if (store.key(i).indexOf(prefix) !== -1) {
|
||||
keysToRemove.push(store.key(i));
|
||||
}
|
||||
}
|
||||
utils.forEach(keysToRemove, function(key) {
|
||||
store.removeItem(key);
|
||||
});
|
||||
} */
|
||||
|
||||
/** Web Storage Store class. */
|
||||
module.exports = WebStorageStore;
|
||||
-1066
File diff suppressed because it is too large
Load Diff
@@ -1,484 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/** @module timeline-window */
|
||||
|
||||
var q = require("q");
|
||||
var EventTimeline = require("./models/event-timeline");
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
var DEBUG = false;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
var debuglog = DEBUG ? console.log.bind(console) : function() {};
|
||||
|
||||
/**
|
||||
* the number of times we ask the server for more events before giving up
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
|
||||
|
||||
/**
|
||||
* Construct a TimelineWindow.
|
||||
*
|
||||
* <p>This abstracts the separate timelines in a Matrix {@link
|
||||
* module:models/room~Room|Room} into a single iterable thing. It keeps track of
|
||||
* the start and endpoints of the window, which can be advanced with the help
|
||||
* of pagination requests.
|
||||
*
|
||||
* <p>Before the window is useful, it must be initialised by calling {@link
|
||||
* module:timeline-window~TimelineWindow#load|load}.
|
||||
*
|
||||
* <p>Note that the window will not automatically extend itself when new events
|
||||
* are received from /sync; you should arrange to call {@link
|
||||
* module:timeline-window~TimelineWindow#paginate|paginate} on {@link
|
||||
* module:client~MatrixClient.event:"Room.timeline"|Room.timeline} events.
|
||||
*
|
||||
* @param {MatrixClient} client MatrixClient to be used for context/pagination
|
||||
* requests.
|
||||
*
|
||||
* @param {Room} room The room to track
|
||||
*
|
||||
* @param {Object} [opts] Configuration options for this window
|
||||
*
|
||||
* @param {number} [opts.windowLimit = 1000] maximum number of events to keep
|
||||
* in the window. If more events are retrieved via pagination requests,
|
||||
* excess events will be dropped from the other end of the window.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function TimelineWindow(client, room, opts) {
|
||||
opts = opts || {};
|
||||
this._client = client;
|
||||
this._room = room;
|
||||
|
||||
// these will be TimelineIndex objects; they delineate the 'start' and
|
||||
// 'end' of the window.
|
||||
//
|
||||
// _start.index is inclusive; _end.index is exclusive.
|
||||
this._start = null;
|
||||
this._end = null;
|
||||
|
||||
this._eventCount = 0;
|
||||
this._windowLimit = opts.windowLimit || 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the window to point at a given event, or the live timeline
|
||||
*
|
||||
* @param {string} [initialEventId] If given, the window will contain the
|
||||
* given event
|
||||
* @param {number} [initialWindowSize = 20] Size of the initial window
|
||||
*
|
||||
* @return {module:client.Promise}
|
||||
*/
|
||||
TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
|
||||
var self = this;
|
||||
initialWindowSize = initialWindowSize || 20;
|
||||
|
||||
// given an EventTimeline, and an event index within it, initialise our
|
||||
// fields so that the event in question is in the middle of the window.
|
||||
var initFields = function(timeline, eventIndex) {
|
||||
var endIndex = Math.min(timeline.getEvents().length,
|
||||
eventIndex + Math.ceil(initialWindowSize / 2));
|
||||
var startIndex = Math.max(0, endIndex - initialWindowSize);
|
||||
self._start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex());
|
||||
self._end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex());
|
||||
self._eventCount = endIndex - startIndex;
|
||||
};
|
||||
|
||||
// We avoid delaying the resolution of the promise by a reactor tick if
|
||||
// we already have the data we need, which is important to keep room-switching
|
||||
// feeling snappy.
|
||||
//
|
||||
// TODO: ideally we'd spot getEventTimeline returning a resolved promise and
|
||||
// skip straight to the find-event loop.
|
||||
if (initialEventId) {
|
||||
return this._client.getEventTimeline(this._room, initialEventId)
|
||||
.then(function(tl) {
|
||||
// make sure that our window includes the event
|
||||
for (var i = 0; i < tl.getEvents().length; i++) {
|
||||
if (tl.getEvents()[i].getId() == initialEventId) {
|
||||
initFields(tl, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error("getEventTimeline result didn't include requested event");
|
||||
});
|
||||
} else {
|
||||
// start with the most recent events
|
||||
var tl = this._room.getLiveTimeline();
|
||||
initFields(tl, tl.getEvents().length);
|
||||
return q();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if this window can be extended
|
||||
*
|
||||
* <p>This returns true if we either have more events, or if we have a
|
||||
* pagination token which means we can paginate in that direction. It does not
|
||||
* necessarily mean that there are more events available in that direction at
|
||||
* this time.
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to check if we can
|
||||
* paginate backwards; EventTimeline.FORWARDS to check if we can go forwards
|
||||
*
|
||||
* @return {boolean} true if we can paginate in the given direction
|
||||
*/
|
||||
TimelineWindow.prototype.canPaginate = function(direction) {
|
||||
var tl;
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
tl = this._start;
|
||||
} else if (direction == EventTimeline.FORWARDS) {
|
||||
tl = this._end;
|
||||
} else {
|
||||
throw new Error("Invalid direction '" + direction + "'");
|
||||
}
|
||||
|
||||
if (!tl) {
|
||||
debuglog("TimelineWindow: no timeline yet");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
if (tl.index > tl.minIndex()) { return true; }
|
||||
} else {
|
||||
if (tl.index < tl.maxIndex()) { return true; }
|
||||
}
|
||||
|
||||
return Boolean(tl.timeline.getNeighbouringTimeline(direction) ||
|
||||
tl.timeline.getPaginationToken(direction));
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempt to extend the window
|
||||
*
|
||||
* @param {string} direction EventTimeline.BACKWARDS to extend the window
|
||||
* backwards (towards older events); EventTimeline.FORWARDS to go forwards.
|
||||
*
|
||||
* @param {number} size number of events to try to extend by. If fewer than this
|
||||
* number are immediately available, then we return immediately rather than
|
||||
* making an API call.
|
||||
*
|
||||
* @param {boolean} [makeRequest = true] whether we should make API calls to
|
||||
* fetch further events if we don't have any at all. (This has no effect if
|
||||
* the room already knows about additional events in the relevant direction,
|
||||
* even if there are fewer than 'size' of them, as we will just return those
|
||||
* we already know about.)
|
||||
*
|
||||
* @param {number} [requestLimit = 5] limit for the number of API requests we
|
||||
* should make.
|
||||
*
|
||||
* @return {module:client.Promise} Resolves to a boolean which is true if more events
|
||||
* were successfully retrieved.
|
||||
*/
|
||||
TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
|
||||
requestLimit) {
|
||||
// Either wind back the message cap (if there are enough events in the
|
||||
// timeline to do so), or fire off a pagination request.
|
||||
|
||||
if (makeRequest === undefined) {
|
||||
makeRequest = true;
|
||||
}
|
||||
|
||||
if (requestLimit === undefined) {
|
||||
requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT;
|
||||
}
|
||||
|
||||
var tl;
|
||||
if (direction == EventTimeline.BACKWARDS) {
|
||||
tl = this._start;
|
||||
} else if (direction == EventTimeline.FORWARDS) {
|
||||
tl = this._end;
|
||||
} else {
|
||||
throw new Error("Invalid direction '" + direction + "'");
|
||||
}
|
||||
|
||||
if (!tl) {
|
||||
debuglog("TimelineWindow: no timeline yet");
|
||||
return q(false);
|
||||
}
|
||||
|
||||
if (tl.pendingPaginate) {
|
||||
return tl.pendingPaginate;
|
||||
}
|
||||
|
||||
// try moving the cap
|
||||
var count = (direction == EventTimeline.BACKWARDS) ?
|
||||
tl.retreat(size) : tl.advance(size);
|
||||
|
||||
if (count) {
|
||||
this._eventCount += count;
|
||||
debuglog("TimelineWindow: increased cap by " + count +
|
||||
" (now " + this._eventCount + ")");
|
||||
// remove some events from the other end, if necessary
|
||||
var excess = this._eventCount - this._windowLimit;
|
||||
if (excess > 0) {
|
||||
this._unpaginate(excess, direction != EventTimeline.BACKWARDS);
|
||||
}
|
||||
return q(true);
|
||||
}
|
||||
|
||||
if (!makeRequest || requestLimit === 0) {
|
||||
// todo: should we return something different to indicate that there
|
||||
// might be more events out there, but we haven't found them yet?
|
||||
return q(false);
|
||||
}
|
||||
|
||||
// try making a pagination request
|
||||
var token = tl.timeline.getPaginationToken(direction);
|
||||
if (!token) {
|
||||
debuglog("TimelineWindow: no token");
|
||||
return q(false);
|
||||
}
|
||||
|
||||
debuglog("TimelineWindow: starting request");
|
||||
var self = this;
|
||||
var prom = this._client.paginateEventTimeline(tl.timeline, {
|
||||
backwards: direction == EventTimeline.BACKWARDS,
|
||||
limit: size
|
||||
}).finally(function() {
|
||||
tl.pendingPaginate = null;
|
||||
}).then(function(r) {
|
||||
debuglog("TimelineWindow: request completed with result " + r);
|
||||
if (!r) {
|
||||
// end of timeline
|
||||
return false;
|
||||
}
|
||||
|
||||
// recurse to advance the index into the results.
|
||||
//
|
||||
// If we don't get any new events, we want to make sure we keep asking
|
||||
// the server for events for as long as we have a valid pagination
|
||||
// token. In particular, we want to know if we've actually hit the
|
||||
// start of the timeline, or if we just happened to know about all of
|
||||
// the events thanks to https://matrix.org/jira/browse/SYN-645.
|
||||
//
|
||||
// On the other hand, we necessarily want to wait forever for the
|
||||
// server to make its mind up about whether there are other events,
|
||||
// because it gives a bad user experience
|
||||
// (https://github.com/vector-im/vector-web/issues/1204).
|
||||
return self.paginate(direction, size, true, requestLimit - 1);
|
||||
});
|
||||
tl.pendingPaginate = prom;
|
||||
return prom;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Trim the window to the windowlimit
|
||||
*
|
||||
* @param {number} delta number of events to remove from the timeline
|
||||
* @param {boolean} startOfTimeline if events should be removed from the start
|
||||
* of the timeline.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
TimelineWindow.prototype._unpaginate = function(delta, startOfTimeline) {
|
||||
var tl = startOfTimeline ? this._start : this._end;
|
||||
|
||||
// sanity-check the delta
|
||||
if (delta > this._eventCount || delta < 0) {
|
||||
throw new Error("Attemting to unpaginate " + delta + " events, but " +
|
||||
"only have " + this._eventCount + " in the timeline");
|
||||
}
|
||||
|
||||
while (delta > 0) {
|
||||
var count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta);
|
||||
if (count <= 0) {
|
||||
// sadness. This shouldn't be possible.
|
||||
throw new Error(
|
||||
"Unable to unpaginate any further, but still have " +
|
||||
this._eventCount + " events");
|
||||
}
|
||||
|
||||
delta -= count;
|
||||
this._eventCount -= count;
|
||||
debuglog("TimelineWindow.unpaginate: dropped " + count +
|
||||
" (now " + this._eventCount + ")");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get a list of the events currently in the window
|
||||
*
|
||||
* @return {MatrixEvent[]} the events in the window
|
||||
*/
|
||||
TimelineWindow.prototype.getEvents = function() {
|
||||
if (!this._start) {
|
||||
// not yet loaded
|
||||
return [];
|
||||
}
|
||||
|
||||
var result = [];
|
||||
|
||||
// iterate through each timeline between this._start and this._end
|
||||
// (inclusive).
|
||||
var timeline = this._start.timeline;
|
||||
while (true) {
|
||||
var events = timeline.getEvents();
|
||||
|
||||
// For the first timeline in the chain, we want to start at
|
||||
// this._start.index. For the last timeline in the chain, we want to
|
||||
// stop before this._end.index. Otherwise, we want to copy all of the
|
||||
// events in the timeline.
|
||||
//
|
||||
// (Note that both this._start.index and this._end.index are relative
|
||||
// to their respective timelines' BaseIndex).
|
||||
//
|
||||
var startIndex = 0, endIndex = events.length;
|
||||
if (timeline === this._start.timeline) {
|
||||
startIndex = this._start.index + timeline.getBaseIndex();
|
||||
}
|
||||
if (timeline === this._end.timeline) {
|
||||
endIndex = this._end.index + timeline.getBaseIndex();
|
||||
}
|
||||
|
||||
for (var i = startIndex; i < endIndex; i++) {
|
||||
result.push(events[i]);
|
||||
}
|
||||
|
||||
// if we're not done, iterate to the next timeline.
|
||||
if (timeline === this._end.timeline) {
|
||||
break;
|
||||
} else {
|
||||
timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* a thing which contains a timeline reference, and an index into it.
|
||||
*
|
||||
* @constructor
|
||||
* @param {EventTimeline} timeline
|
||||
* @param {number} index
|
||||
* @private
|
||||
*/
|
||||
function TimelineIndex(timeline, index) {
|
||||
this.timeline = timeline;
|
||||
|
||||
// the indexes are relative to BaseIndex, so could well be negative.
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number} the minimum possible value for the index in the current
|
||||
* timeline
|
||||
*/
|
||||
TimelineIndex.prototype.minIndex = function() {
|
||||
return this.timeline.getBaseIndex() * -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {number} the maximum possible value for the index in the current
|
||||
* timeline (exclusive - ie, it actually returns one more than the index
|
||||
* of the last element).
|
||||
*/
|
||||
TimelineIndex.prototype.maxIndex = function() {
|
||||
return this.timeline.getEvents().length - this.timeline.getBaseIndex();
|
||||
};
|
||||
|
||||
/**
|
||||
* Try move the index forward, or into the neighbouring timeline
|
||||
*
|
||||
* @param {number} delta number of events to advance by
|
||||
* @return {number} number of events successfully advanced by
|
||||
*/
|
||||
TimelineIndex.prototype.advance = function(delta) {
|
||||
if (!delta) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// first try moving the index in the current timeline. See if there is room
|
||||
// to do so.
|
||||
var cappedDelta;
|
||||
if (delta < 0) {
|
||||
// we want to wind the index backwards.
|
||||
//
|
||||
// (this.minIndex() - this.index) is a negative number whose magnitude
|
||||
// is the amount of room we have to wind back the index in the current
|
||||
// timeline. We cap delta to this quantity.
|
||||
cappedDelta = Math.max(delta, this.minIndex() - this.index);
|
||||
if (cappedDelta < 0) {
|
||||
this.index += cappedDelta;
|
||||
return cappedDelta;
|
||||
}
|
||||
} else {
|
||||
// we want to wind the index forwards.
|
||||
//
|
||||
// (this.maxIndex() - this.index) is a (positive) number whose magnitude
|
||||
// is the amount of room we have to wind forward the index in the current
|
||||
// timeline. We cap delta to this quantity.
|
||||
cappedDelta = Math.min(delta, this.maxIndex() - this.index);
|
||||
if (cappedDelta > 0) {
|
||||
this.index += cappedDelta;
|
||||
return cappedDelta;
|
||||
}
|
||||
}
|
||||
|
||||
// the index is already at the start/end of the current timeline.
|
||||
//
|
||||
// next see if there is a neighbouring timeline to switch to.
|
||||
var neighbour = this.timeline.getNeighbouringTimeline(
|
||||
delta < 0 ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS);
|
||||
if (neighbour) {
|
||||
this.timeline = neighbour;
|
||||
if (delta < 0) {
|
||||
this.index = this.maxIndex();
|
||||
} else {
|
||||
this.index = this.minIndex();
|
||||
}
|
||||
|
||||
debuglog("paginate: switched to new neighbour");
|
||||
|
||||
// recurse, using the next timeline
|
||||
return this.advance(delta);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Try move the index backwards, or into the neighbouring timeline
|
||||
*
|
||||
* @param {number} delta number of events to retreat by
|
||||
* @return {number} number of events successfully retreated by
|
||||
*/
|
||||
TimelineIndex.prototype.retreat = function(delta) {
|
||||
return this.advance(delta * -1) * -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* The TimelineWindow class.
|
||||
*/
|
||||
module.exports.TimelineWindow = TimelineWindow;
|
||||
|
||||
/**
|
||||
* The TimelineIndex class. exported here for unit testing.
|
||||
*/
|
||||
module.exports.TimelineIndex = TimelineIndex;
|
||||
-631
@@ -1,631 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
"use strict";
|
||||
/**
|
||||
* This is an internal module.
|
||||
* @module utils
|
||||
*/
|
||||
|
||||
/**
|
||||
* Encode a dictionary of query parameters.
|
||||
* @param {Object} params A dict of key/values to encode e.g.
|
||||
* {"foo": "bar", "baz": "taz"}
|
||||
* @return {string} The encoded string e.g. foo=bar&baz=taz
|
||||
*/
|
||||
module.exports.encodeParams = function(params) {
|
||||
var qs = "";
|
||||
for (var key in params) {
|
||||
if (!params.hasOwnProperty(key)) { continue; }
|
||||
qs += "&" + encodeURIComponent(key) + "=" +
|
||||
encodeURIComponent(params[key]);
|
||||
}
|
||||
return qs.substring(1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Encodes a URI according to a set of template variables. Variables will be
|
||||
* passed through encodeURIComponent.
|
||||
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
|
||||
* @param {Object} variables The key/value pairs to replace the template
|
||||
* variables with. E.g. { "$bar": "baz" }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
module.exports.encodeUri = function(pathTemplate, variables) {
|
||||
for (var key in variables) {
|
||||
if (!variables.hasOwnProperty(key)) { continue; }
|
||||
pathTemplate = pathTemplate.replace(
|
||||
key, encodeURIComponent(variables[key])
|
||||
);
|
||||
}
|
||||
return pathTemplate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Applies a map function to the given array.
|
||||
* @param {Array} array The array to apply the function to.
|
||||
* @param {Function} fn The function that will be invoked for each element in
|
||||
* the array with the signature <code>fn(element){...}</code>
|
||||
* @return {Array} A new array with the results of the function.
|
||||
*/
|
||||
module.exports.map = function(array, fn) {
|
||||
var results = new Array(array.length);
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
results[i] = fn(array[i]);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* Applies a filter function to the given array.
|
||||
* @param {Array} array The array to apply the function to.
|
||||
* @param {Function} fn The function that will be invoked for each element in
|
||||
* the array. It should return true to keep the element. The function signature
|
||||
* looks like <code>fn(element, index, array){...}</code>.
|
||||
* @return {Array} A new array with the results of the function.
|
||||
*/
|
||||
module.exports.filter = function(array, fn) {
|
||||
var results = [];
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
if (fn(array[i], i, array)) {
|
||||
results.push(array[i]);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the keys for an object. Same as <code>Object.keys()</code>.
|
||||
* @param {Object} obj The object to get the keys for.
|
||||
* @return {string[]} The keys of the object.
|
||||
*/
|
||||
module.exports.keys = function(obj) {
|
||||
var keys = [];
|
||||
for (var key in obj) {
|
||||
if (!obj.hasOwnProperty(key)) { continue; }
|
||||
keys.push(key);
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the values for an object.
|
||||
* @param {Object} obj The object to get the values for.
|
||||
* @return {Array<*>} The values of the object.
|
||||
*/
|
||||
module.exports.values = function(obj) {
|
||||
var values = [];
|
||||
for (var key in obj) {
|
||||
if (!obj.hasOwnProperty(key)) { continue; }
|
||||
values.push(obj[key]);
|
||||
}
|
||||
return values;
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoke a function for each item in the array.
|
||||
* @param {Array} array The array.
|
||||
* @param {Function} fn The function to invoke for each element. Has the
|
||||
* function signature <code>fn(element, index)</code>.
|
||||
*/
|
||||
module.exports.forEach = function(array, fn) {
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
fn(array[i], i);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The findElement() method returns a value in the array, if an element in the array
|
||||
* satisfies (returns true) the provided testing function. Otherwise undefined
|
||||
* is returned.
|
||||
* @param {Array} array The array.
|
||||
* @param {Function} fn Function to execute on each value in the array, with the
|
||||
* function signature <code>fn(element, index, array)</code>
|
||||
* @param {boolean} reverse True to search in reverse order.
|
||||
* @return {*} The first value in the array which returns <code>true</code> for
|
||||
* the given function.
|
||||
*/
|
||||
module.exports.findElement = function(array, fn, reverse) {
|
||||
var i;
|
||||
if (reverse) {
|
||||
for (i = array.length - 1; i >= 0; i--) {
|
||||
if (fn(array[i], i, array)) {
|
||||
return array[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (i = 0; i < array.length; i++) {
|
||||
if (fn(array[i], i, array)) {
|
||||
return array[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The removeElement() method removes the first element in the array that
|
||||
* satisfies (returns true) the provided testing function.
|
||||
* @param {Array} array The array.
|
||||
* @param {Function} fn Function to execute on each value in the array, with the
|
||||
* function signature <code>fn(element, index, array)</code>. Return true to
|
||||
* remove this element and break.
|
||||
* @param {boolean} reverse True to search in reverse order.
|
||||
* @return {boolean} True if an element was removed.
|
||||
*/
|
||||
module.exports.removeElement = function(array, fn, reverse) {
|
||||
var i;
|
||||
var removed;
|
||||
if (reverse) {
|
||||
for (i = array.length - 1; i >= 0; i--) {
|
||||
if (fn(array[i], i, array)) {
|
||||
removed = array[i];
|
||||
array.splice(i, 1);
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (i = 0; i < array.length; i++) {
|
||||
if (fn(array[i], i, array)) {
|
||||
removed = array[i];
|
||||
array.splice(i, 1);
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the given thing is a function.
|
||||
* @param {*} value The thing to check.
|
||||
* @return {boolean} True if it is a function.
|
||||
*/
|
||||
module.exports.isFunction = function(value) {
|
||||
return Object.prototype.toString.call(value) == "[object Function]";
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the given thing is an array.
|
||||
* @param {*} value The thing to check.
|
||||
* @return {boolean} True if it is an array.
|
||||
*/
|
||||
module.exports.isArray = function(value) {
|
||||
return Boolean(value && value.constructor === Array);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks that the given object has the specified keys.
|
||||
* @param {Object} obj The object to check.
|
||||
* @param {string[]} keys The list of keys that 'obj' must have.
|
||||
* @throws If the object is missing keys.
|
||||
*/
|
||||
module.exports.checkObjectHasKeys = function(obj, keys) {
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (!obj.hasOwnProperty(keys[i])) {
|
||||
throw new Error("Missing required key: " + keys[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks that the given object has no extra keys other than the specified ones.
|
||||
* @param {Object} obj The object to check.
|
||||
* @param {string[]} allowedKeys The list of allowed key names.
|
||||
* @throws If there are extra keys.
|
||||
*/
|
||||
module.exports.checkObjectHasNoAdditionalKeys = function(obj, allowedKeys) {
|
||||
for (var key in obj) {
|
||||
if (!obj.hasOwnProperty(key)) { continue; }
|
||||
if (allowedKeys.indexOf(key) === -1) {
|
||||
throw new Error("Unknown key: " + key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deep copy the given object. The object MUST NOT have circular references and
|
||||
* MUST NOT have functions.
|
||||
* @param {Object} obj The object to deep copy.
|
||||
* @return {Object} A copy of the object without any references to the original.
|
||||
*/
|
||||
module.exports.deepCopy = function(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare two objects for equality. The objects MUST NOT have circular references.
|
||||
*
|
||||
* @param {Object} x The first object to compare.
|
||||
* @param {Object} y The second object to compare.
|
||||
*
|
||||
* @return {boolean} true if the two objects are equal
|
||||
*/
|
||||
var deepCompare = module.exports.deepCompare = function(x, y) {
|
||||
// Inspired by
|
||||
// http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249
|
||||
|
||||
// Compare primitives and functions.
|
||||
// Also check if both arguments link to the same object.
|
||||
if (x === y) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof x !== typeof y) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// special-case NaN (since NaN !== NaN)
|
||||
if (typeof x === 'number' && isNaN(x) && isNaN(y)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// special-case null (since typeof null == 'object', but null.constructor
|
||||
// throws)
|
||||
if (x === null || y === null) {
|
||||
return x === y;
|
||||
}
|
||||
|
||||
// everything else is either an unequal primitive, or an object
|
||||
if (!(x instanceof Object)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check they are the same type of object
|
||||
if (x.constructor !== y.constructor || x.prototype !== y.prototype) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// special-casing for some special types of object
|
||||
if (x instanceof RegExp || x instanceof Date) {
|
||||
return x.toString() === y.toString();
|
||||
}
|
||||
|
||||
// the object algorithm works for Array, but it's sub-optimal.
|
||||
if (x instanceof Array) {
|
||||
if (x.length !== y.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < x.length; i++) {
|
||||
if (!deepCompare(x[i], y[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// disable jshint "The body of a for in should be wrapped in an if
|
||||
// statement"
|
||||
/* jshint -W089 */
|
||||
|
||||
// check that all of y's direct keys are in x
|
||||
var p;
|
||||
for (p in y) {
|
||||
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// finally, compare each of x's keys with y
|
||||
for (p in y) {
|
||||
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
|
||||
return false;
|
||||
}
|
||||
if (!deepCompare(x[p], y[p])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* jshint +W089 */
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Run polyfills to add Array.map and Array.filter if they are missing.
|
||||
*/
|
||||
module.exports.runPolyfills = function() {
|
||||
// Array.prototype.filter
|
||||
// ========================================================
|
||||
// SOURCE:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
|
||||
if (!Array.prototype.filter) {
|
||||
Array.prototype.filter = function(fun/*, thisArg*/) {
|
||||
|
||||
if (this === void 0 || this === null) {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
var t = Object(this);
|
||||
var len = t.length >>> 0;
|
||||
if (typeof fun !== 'function') {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
var res = [];
|
||||
var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
|
||||
for (var i = 0; i < len; i++) {
|
||||
if (i in t) {
|
||||
var val = t[i];
|
||||
|
||||
// NOTE: Technically this should Object.defineProperty at
|
||||
// the next index, as push can be affected by
|
||||
// properties on Object.prototype and Array.prototype.
|
||||
// But that method's new, and collisions should be
|
||||
// rare, so use the more-compatible alternative.
|
||||
if (fun.call(thisArg, val, i, t)) {
|
||||
res.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
// Array.prototype.map
|
||||
// ========================================================
|
||||
// SOURCE:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
|
||||
// Production steps of ECMA-262, Edition 5, 15.4.4.19
|
||||
// Reference: http://es5.github.io/#x15.4.4.19
|
||||
if (!Array.prototype.map) {
|
||||
|
||||
Array.prototype.map = function(callback, thisArg) {
|
||||
|
||||
var T, A, k;
|
||||
|
||||
if (this === null || this === undefined) {
|
||||
throw new TypeError(' this is null or not defined');
|
||||
}
|
||||
|
||||
// 1. Let O be the result of calling ToObject passing the |this|
|
||||
// value as the argument.
|
||||
var O = Object(this);
|
||||
|
||||
// 2. Let lenValue be the result of calling the Get internal
|
||||
// method of O with the argument "length".
|
||||
// 3. Let len be ToUint32(lenValue).
|
||||
var len = O.length >>> 0;
|
||||
|
||||
// 4. If IsCallable(callback) is false, throw a TypeError exception.
|
||||
// See: http://es5.github.com/#x9.11
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError(callback + ' is not a function');
|
||||
}
|
||||
|
||||
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||
if (arguments.length > 1) {
|
||||
T = thisArg;
|
||||
}
|
||||
|
||||
// 6. Let A be a new array created as if by the expression new Array(len)
|
||||
// where Array is the standard built-in constructor with that name and
|
||||
// len is the value of len.
|
||||
A = new Array(len);
|
||||
|
||||
// 7. Let k be 0
|
||||
k = 0;
|
||||
|
||||
// 8. Repeat, while k < len
|
||||
while (k < len) {
|
||||
|
||||
var kValue, mappedValue;
|
||||
|
||||
// a. Let Pk be ToString(k).
|
||||
// This is implicit for LHS operands of the in operator
|
||||
// b. Let kPresent be the result of calling the HasProperty internal
|
||||
// method of O with argument Pk.
|
||||
// This step can be combined with c
|
||||
// c. If kPresent is true, then
|
||||
if (k in O) {
|
||||
|
||||
// i. Let kValue be the result of calling the Get internal
|
||||
// method of O with argument Pk.
|
||||
kValue = O[k];
|
||||
|
||||
// ii. Let mappedValue be the result of calling the Call internal
|
||||
// method of callback with T as the this value and argument
|
||||
// list containing kValue, k, and O.
|
||||
mappedValue = callback.call(T, kValue, k, O);
|
||||
|
||||
// iii. Call the DefineOwnProperty internal method of A with arguments
|
||||
// Pk, Property Descriptor
|
||||
// { Value: mappedValue,
|
||||
// Writable: true,
|
||||
// Enumerable: true,
|
||||
// Configurable: true },
|
||||
// and false.
|
||||
|
||||
// In browsers that support Object.defineProperty, use the following:
|
||||
// Object.defineProperty(A, k, {
|
||||
// value: mappedValue,
|
||||
// writable: true,
|
||||
// enumerable: true,
|
||||
// configurable: true
|
||||
// });
|
||||
|
||||
// For best browser support, use the following:
|
||||
A[k] = mappedValue;
|
||||
}
|
||||
// d. Increase k by 1.
|
||||
k++;
|
||||
}
|
||||
|
||||
// 9. return A
|
||||
return A;
|
||||
};
|
||||
}
|
||||
|
||||
// Array.prototype.forEach
|
||||
// ========================================================
|
||||
// SOURCE:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
|
||||
// Production steps of ECMA-262, Edition 5, 15.4.4.18
|
||||
// Reference: http://es5.github.io/#x15.4.4.18
|
||||
if (!Array.prototype.forEach) {
|
||||
|
||||
Array.prototype.forEach = function(callback, thisArg) {
|
||||
|
||||
var T, k;
|
||||
|
||||
if (this === null || this === undefined) {
|
||||
throw new TypeError(' this is null or not defined');
|
||||
}
|
||||
|
||||
// 1. Let O be the result of calling ToObject passing the |this| value as the
|
||||
// argument.
|
||||
var O = Object(this);
|
||||
|
||||
// 2. Let lenValue be the result of calling the Get internal method of O with the
|
||||
// argument "length".
|
||||
// 3. Let len be ToUint32(lenValue).
|
||||
var len = O.length >>> 0;
|
||||
|
||||
// 4. If IsCallable(callback) is false, throw a TypeError exception.
|
||||
// See: http://es5.github.com/#x9.11
|
||||
if (typeof callback !== "function") {
|
||||
throw new TypeError(callback + ' is not a function');
|
||||
}
|
||||
|
||||
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||
if (arguments.length > 1) {
|
||||
T = thisArg;
|
||||
}
|
||||
|
||||
// 6. Let k be 0
|
||||
k = 0;
|
||||
|
||||
// 7. Repeat, while k < len
|
||||
while (k < len) {
|
||||
|
||||
var kValue;
|
||||
|
||||
// a. Let Pk be ToString(k).
|
||||
// This is implicit for LHS operands of the in operator
|
||||
// b. Let kPresent be the result of calling the HasProperty internal
|
||||
// method of O with
|
||||
// argument Pk.
|
||||
// This step can be combined with c
|
||||
// c. If kPresent is true, then
|
||||
if (k in O) {
|
||||
|
||||
// i. Let kValue be the result of calling the Get internal method of O with
|
||||
// argument Pk
|
||||
kValue = O[k];
|
||||
|
||||
// ii. Call the Call internal method of callback with T as the this value and
|
||||
// argument list containing kValue, k, and O.
|
||||
callback.call(T, kValue, k, O);
|
||||
}
|
||||
// d. Increase k by 1.
|
||||
k++;
|
||||
}
|
||||
// 8. return undefined
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherit the prototype methods from one constructor into another. This is a
|
||||
* port of the Node.js implementation with an Object.create polyfill.
|
||||
*
|
||||
* @param {function} ctor Constructor function which needs to inherit the
|
||||
* prototype.
|
||||
* @param {function} superCtor Constructor function to inherit prototype from.
|
||||
*/
|
||||
module.exports.inherits = function(ctor, superCtor) {
|
||||
// Add Object.create polyfill for IE8
|
||||
// Source:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript
|
||||
// /Reference/Global_Objects/Object/create#Polyfill
|
||||
if (typeof Object.create != 'function') {
|
||||
// Production steps of ECMA-262, Edition 5, 15.2.3.5
|
||||
// Reference: http://es5.github.io/#x15.2.3.5
|
||||
Object.create = (function() {
|
||||
// To save on memory, use a shared constructor
|
||||
function Temp() {}
|
||||
|
||||
// make a safe reference to Object.prototype.hasOwnProperty
|
||||
var hasOwn = Object.prototype.hasOwnProperty;
|
||||
|
||||
return function(O) {
|
||||
// 1. If Type(O) is not Object or Null throw a TypeError exception.
|
||||
if (typeof O != 'object') {
|
||||
throw new TypeError('Object prototype may only be an Object or null');
|
||||
}
|
||||
|
||||
// 2. Let obj be the result of creating a new object as if by the
|
||||
// expression new Object() where Object is the standard built-in
|
||||
// constructor with that name
|
||||
// 3. Set the [[Prototype]] internal property of obj to O.
|
||||
Temp.prototype = O;
|
||||
var obj = new Temp();
|
||||
Temp.prototype = null; // Let's not keep a stray reference to O...
|
||||
|
||||
// 4. If the argument Properties is present and not undefined, add
|
||||
// own properties to obj as if by calling the standard built-in
|
||||
// function Object.defineProperties with arguments obj and
|
||||
// Properties.
|
||||
if (arguments.length > 1) {
|
||||
// Object.defineProperties does ToObject on its first argument.
|
||||
var Properties = Object(arguments[1]);
|
||||
for (var prop in Properties) {
|
||||
if (hasOwn.call(Properties, prop)) {
|
||||
obj[prop] = Properties[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Return obj
|
||||
return obj;
|
||||
};
|
||||
})();
|
||||
}
|
||||
// END polyfill
|
||||
|
||||
// Add util.inherits from Node.js
|
||||
// Source:
|
||||
// https://github.com/joyent/node/blob/master/lib/util.js
|
||||
// Copyright Joyent, Inc. and other Node contributors.
|
||||
//
|
||||
// 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.
|
||||
ctor.super_ = superCtor;
|
||||
ctor.prototype = Object.create(superCtor.prototype, {
|
||||
constructor: {
|
||||
value: ctor,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true
|
||||
}
|
||||
});
|
||||
};
|
||||
-1171
File diff suppressed because it is too large
Load Diff
+113
-23
@@ -1,40 +1,130 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.5.4",
|
||||
"version": "19.3.0-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"node": ">=12.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --captureExceptions",
|
||||
"check": "jasmine-node spec --verbose --junitreport --captureExceptions",
|
||||
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
|
||||
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js --ignore-missing",
|
||||
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
|
||||
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r spec/ -r lib/",
|
||||
"release": "npm run build && mkdir -p dist/$npm_package_version && uglifyjs -c -m -o dist/$npm_package_version/browser-matrix-$npm_package_version.min.js dist/browser-matrix-dev.js && cp dist/browser-matrix-dev.js dist/$npm_package_version/browser-matrix-$npm_package_version.js",
|
||||
"prepublish": "git rev-parse HEAD > git-revision.txt",
|
||||
"version": "npm run release && git add dist/$npm_package_version"
|
||||
"prepublishOnly": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
|
||||
"clean": "rimraf lib dist",
|
||||
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
|
||||
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
|
||||
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
|
||||
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 0 src spec",
|
||||
"lint:js-fix": "eslint --fix src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/matrix-org/matrix-js-sdk"
|
||||
},
|
||||
"keywords": [
|
||||
"matrix-org"
|
||||
],
|
||||
"browser": "browser-index.js",
|
||||
"main": "./lib/index.js",
|
||||
"browser": "./lib/browser-index.js",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_src_browser": "./src/browser-index.js",
|
||||
"matrix_lib_main": "./lib/index.js",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache 2.0",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"dist",
|
||||
"lib",
|
||||
"src",
|
||||
"git-revision.txt",
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.rst",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"package.json",
|
||||
"release.sh"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"another-json": "^0.2.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"browserify": "^10.2.3",
|
||||
"q": "^1.4.1",
|
||||
"request": "^2.53.0"
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"matrix-events-sdk": "^0.0.1-beta.7",
|
||||
"p-retry": "4",
|
||||
"qs": "^6.9.6",
|
||||
"request": "^2.88.2",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"watchify": "^3.2.1",
|
||||
"istanbul": "^0.3.13",
|
||||
"jasmine-node": "^1.14.5",
|
||||
"jshint": "^2.8.0",
|
||||
"jsdoc": "^3.4.0",
|
||||
"uglifyjs": "^2.4.10"
|
||||
}
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/jest": "^28.0.0",
|
||||
"@types/node": "16",
|
||||
"@types/request": "^2.48.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"allchange": "^1.0.6",
|
||||
"babel-jest": "^28.0.0",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.4.0-beta.9",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "8.20.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-matrix-org": "^0.5.0",
|
||||
"exorcist": "^2.0.0",
|
||||
"fake-indexeddb": "^4.0.0",
|
||||
"jest": "^28.0.0",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jest-sonar-reporter": "^2.0.0",
|
||||
"jsdoc": "^3.6.6",
|
||||
"matrix-mock-request": "^2.1.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typescript": "^4.5.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"testMatch": [
|
||||
"<rootDir>/spec/**/*.spec.{js,ts}"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text-summary",
|
||||
"lcov"
|
||||
],
|
||||
"testResultsProcessor": "jest-sonar-reporter"
|
||||
},
|
||||
"jestSonar": {
|
||||
"reportPath": "coverage",
|
||||
"sonar56x": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
+301
-49
@@ -1,14 +1,35 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script to perform a release of matrix-js-sdk. Performs the steps documented
|
||||
# in RELEASING.md
|
||||
# Script to perform a release of matrix-js-sdk and downstream projects.
|
||||
#
|
||||
# Requires github-changelog-generator; to install, do
|
||||
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
|
||||
# Requires:
|
||||
# github-changelog-generator; install via:
|
||||
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
|
||||
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
|
||||
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
|
||||
# npm; typically installed by Node.js
|
||||
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
|
||||
#
|
||||
# Note: this script is also used to release matrix-react-sdk and element-web.
|
||||
|
||||
set -e
|
||||
|
||||
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
|
||||
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
|
||||
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
|
||||
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
|
||||
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
|
||||
echo "hub version 2.5 is required, you have $HUB_VERSION_MAJOR.$HUB_VERSION_MINOR installed"
|
||||
exit
|
||||
fi
|
||||
else
|
||||
echo "hub is required: please install it"
|
||||
exit
|
||||
fi
|
||||
npm --version > /dev/null || (echo "npm is required: please install it"; kill $$)
|
||||
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
|
||||
|
||||
USAGE="$0 [-x] [-c changelog_file] vX.Y.Z"
|
||||
|
||||
help() {
|
||||
cat <<EOF
|
||||
@@ -16,14 +37,32 @@ $USAGE
|
||||
|
||||
-c changelog_file: specify name of file containing changelog
|
||||
-x: skip updating the changelog
|
||||
-z: skip generating the jsdoc
|
||||
-n: skip publish to NPM
|
||||
EOF
|
||||
}
|
||||
|
||||
ret=0
|
||||
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
|
||||
if [ "$ret" -eq 0 ]; then
|
||||
echo "package.json contains develop dependencies. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet --cached HEAD; then
|
||||
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
if ! git diff-files --quiet; then
|
||||
echo "this git checkout has uncommitted changes. Refusing to release."
|
||||
exit
|
||||
fi
|
||||
|
||||
skip_changelog=
|
||||
skip_jsdoc=
|
||||
skip_npm=
|
||||
changelog_file="CHANGELOG.md"
|
||||
while getopts hc:xz f; do
|
||||
expected_npm_user="matrixdotorg"
|
||||
while getopts hc:u:xzn f; do
|
||||
case $f in
|
||||
h)
|
||||
help
|
||||
@@ -35,8 +74,11 @@ while getopts hc:xz f; do
|
||||
x)
|
||||
skip_changelog=1
|
||||
;;
|
||||
z)
|
||||
skip_jsdoc=1
|
||||
n)
|
||||
skip_npm=1
|
||||
;;
|
||||
u)
|
||||
expected_npm_user="$OPTARG"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
@@ -47,28 +89,58 @@ if [ $# -ne 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# We use Git branch / commit dependencies for some packages, and Yarn seems
|
||||
# to have a hard time getting that right. See also
|
||||
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
|
||||
# global cache here to ensure we get the right thing.
|
||||
yarn cache clean
|
||||
# Ensure all dependencies are updated
|
||||
yarn install --ignore-scripts --pure-lockfile
|
||||
|
||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
||||
# defined options and semantics than `yarn` for writing to the registry.
|
||||
if [ -z "$skip_npm" ]; then
|
||||
actual_npm_user=`npm whoami`;
|
||||
if [ $expected_npm_user != $actual_npm_user ]; then
|
||||
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
tag="v${release}"
|
||||
rel_branch="release-$tag"
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
if ! command -v update_changelog >/dev/null 2>&1; then
|
||||
echo "release.sh requires github-changelog-generator. Try:" >&2
|
||||
echo " pip install git+https://github.com/matrix-org/github-changelog-generator.git" >&2
|
||||
exit 1
|
||||
fi
|
||||
prerelease=0
|
||||
# We check if this build is a prerelease by looking to
|
||||
# see if the version has a hyphen in it. Crude,
|
||||
# but semver doesn't support postreleases so anything
|
||||
# with a hyphen is a prerelease.
|
||||
echo $release | grep -q '-' && prerelease=1
|
||||
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
echo Making a PRE-RELEASE
|
||||
else
|
||||
read -p "Making a FINAL RELEASE, press enter to continue " REPLY
|
||||
fi
|
||||
|
||||
# we might already be on the release branch, in which case, yay
|
||||
if [ $(git symbolic-ref --short HEAD) != "$rel_branch" ]; then
|
||||
# We might already be on the release branch, in which case, yay
|
||||
# If we're on any branch starting with 'release', or the staging branch
|
||||
# we don't create a separate release branch (this allows us to use the same
|
||||
# release branch for releases and release candidates).
|
||||
curbranch=$(git symbolic-ref --short HEAD)
|
||||
if [[ "$curbranch" != release* && "$curbranch" != "staging" ]]; then
|
||||
echo "Creating release branch"
|
||||
git checkout -b "$rel_branch"
|
||||
else
|
||||
echo "Using current branch ($curbranch) for release"
|
||||
rel_branch=$curbranch
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
echo "Generating changelog"
|
||||
update_changelog -f "$changelog_file" "$release"
|
||||
yarn run allchange "$release"
|
||||
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
|
||||
|
||||
if [ -n "$(git ls-files --modified $changelog_file)" ]; then
|
||||
@@ -76,45 +148,225 @@ if [ -z "$skip_changelog" ]; then
|
||||
git commit "$changelog_file" -m "Prepare changelog for $tag"
|
||||
fi
|
||||
fi
|
||||
latest_changes=`mktemp`
|
||||
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
|
||||
|
||||
set -x
|
||||
|
||||
# Bump package.json, build the dist, and tag
|
||||
echo "npm version"
|
||||
npm version "$release"
|
||||
# Bump package.json and build the dist
|
||||
echo "yarn version"
|
||||
# yarn version will automatically commit its modification
|
||||
# and make a release tag. We don't want it to create the tag
|
||||
# because it can only sign with the default key, but we can
|
||||
# only turn off both of these behaviours, so we have to
|
||||
# manually commit the result.
|
||||
yarn version --no-git-tag-version --new-version "$release"
|
||||
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
echo "generating jsdocs"
|
||||
npm run gendoc
|
||||
# For the published and dist versions of the package, we copy the
|
||||
# `matrix_lib_main` and `matrix_lib_typings` fields to `main` and `typings` (if
|
||||
# they exist). This small bit of gymnastics allows us to use the TypeScript
|
||||
# source directly for development without needing to build before linting or
|
||||
# testing.
|
||||
for i in main typings
|
||||
do
|
||||
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
||||
if [ "$lib_value" != "null" ]; then
|
||||
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
fi
|
||||
done
|
||||
|
||||
echo "copying jsdocs to gh-pages branch"
|
||||
git checkout gh-pages
|
||||
git pull
|
||||
cp -a ".jsdoc/matrix-js-sdk/$release" .
|
||||
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
|
||||
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
|
||||
$release index.html
|
||||
git add "$release"
|
||||
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
|
||||
# commit yarn.lock if it exists, is versioned, and is modified
|
||||
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
|
||||
then
|
||||
pkglock='yarn.lock'
|
||||
else
|
||||
pkglock=''
|
||||
fi
|
||||
git commit package.json $pkglock -m "$tag"
|
||||
|
||||
|
||||
# figure out if we should be signing this release
|
||||
signing_id=
|
||||
if [ -f release_config.yaml ]; then
|
||||
result=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']" 2> /dev/null || true`
|
||||
if [ "$?" -eq 0 ]; then
|
||||
signing_id=$result
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# If there is a 'dist' script in the package.json,
|
||||
# run it in a separate checkout of the project, then
|
||||
# upload any files in the 'dist' directory as release
|
||||
# assets.
|
||||
# We make a completely separate checkout to be sure
|
||||
# we're using released versions of the dependencies
|
||||
# (rather than whatever we're pulling in from yarn link)
|
||||
assets=''
|
||||
dodist=0
|
||||
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
|
||||
if [ $dodist -eq 0 ]; then
|
||||
projdir=`pwd`
|
||||
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
|
||||
echo "Building distribution copy in $builddir"
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
git checkout "$rel_branch"
|
||||
yarn install --pure-lockfile
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
DIST_VERSION="$tag" yarn dist
|
||||
|
||||
popd
|
||||
|
||||
for i in "$builddir"/dist/*; do
|
||||
assets="$assets -a $i"
|
||||
if [ -n "$signing_id" ]
|
||||
then
|
||||
gpg -u "$signing_id" --armor --output "$i".asc --detach-sig "$i"
|
||||
assets="$assets -a $i.asc"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signed tag
|
||||
# gnupg seems to fail to get the right tty device unless we set it here
|
||||
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
|
||||
else
|
||||
git tag -a -F "${latest_changes}" "$tag"
|
||||
fi
|
||||
|
||||
# push the tag and the release branch
|
||||
git push origin "$rel_branch" "$tag"
|
||||
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signature for the source tarball.
|
||||
#
|
||||
# github will make us a tarball from the tag - we want to create a
|
||||
# signature for it, which means that first of all we need to check that
|
||||
# it's correct.
|
||||
#
|
||||
# we can't deterministically build exactly the same tarball, due to
|
||||
# differences in gzip implementation - but we *can* build the same tar - so
|
||||
# the easiest way to check the validity of the tarball from git is to unzip
|
||||
# it and compare it with our own idea of what the tar should look like.
|
||||
|
||||
# This uses git archive which seems to be what github uses. Specifically,
|
||||
# the header fields are set in the same way: same file mode, uid & gid
|
||||
# both zero and mtime set to the timestamp of the commit that the tag
|
||||
# references. Also note that this puts the commit into the tar headers
|
||||
# and can be extracted with gunzip -c foo.tar.gz | git get-tar-commit-id
|
||||
|
||||
# the name of the sig file we want to create
|
||||
source_sigfile="${tag}-src.tar.gz.asc"
|
||||
|
||||
tarfile="$tag.tar.gz"
|
||||
gh_project_url=$(git remote get-url origin |
|
||||
sed -e 's#^git@github\.com:#https://github.com/#' \
|
||||
-e 's#^git\+ssh://git@github\.com/#https://github.com/#' \
|
||||
-e 's/\.git$//')
|
||||
project_name="${gh_project_url##*/}"
|
||||
curl -L "${gh_project_url}/archive/${tarfile}" -o "${tarfile}"
|
||||
|
||||
# unzip it and compare it with the tar we would generate
|
||||
if ! cmp --silent <(gunzip -c $tarfile) \
|
||||
<(git archive --format tar --prefix="${project_name}-${release}/" "$tag"); then
|
||||
|
||||
# we don't bail out here, because really it's more likely that our comparison
|
||||
# screwed up and it's super annoying to abort the script at this point.
|
||||
cat >&2 <<EOF
|
||||
!!!!!!!!!!!!!!!!!
|
||||
!!!! WARNING !!!!
|
||||
|
||||
Mismatch between our own tarfile and that generated by github: not signing
|
||||
source tarball.
|
||||
|
||||
To resolve, determine if $tarfile is correct, and if so sign it with gpg and
|
||||
attach it to the release as $source_sigfile.
|
||||
|
||||
!!!!!!!!!!!!!!!!!
|
||||
EOF
|
||||
else
|
||||
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig "$tarfile"
|
||||
assets="$assets -a $source_sigfile"
|
||||
fi
|
||||
fi
|
||||
|
||||
hubflags=''
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
hubflags='-p'
|
||||
fi
|
||||
|
||||
release_text=`mktemp`
|
||||
echo "$tag" > "${release_text}"
|
||||
echo >> "${release_text}"
|
||||
cat "${latest_changes}" >> "${release_text}"
|
||||
hub release create $hubflags $assets -F "${release_text}" "$tag"
|
||||
|
||||
if [ $dodist -eq 0 ]; then
|
||||
rm -rf "$builddir"
|
||||
fi
|
||||
rm "${release_text}"
|
||||
rm "${latest_changes}"
|
||||
|
||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
||||
# defined options and semantics than `yarn` for writing to the registry.
|
||||
# Tag both releases and prereleases as `next` so the last stable release remains
|
||||
# the default.
|
||||
if [ -z "$skip_npm" ]; then
|
||||
npm publish --tag next
|
||||
if [ $prerelease -eq 0 ]; then
|
||||
# For a release, also add the default `latest` tag.
|
||||
package=$(cat package.json | jq -er .name)
|
||||
npm dist-tag add "$package@$release" latest
|
||||
fi
|
||||
fi
|
||||
|
||||
# if it is a pre-release, leave it on the release branch for now.
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
git checkout "$rel_branch"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# merge release branch to master
|
||||
echo "updating master branch"
|
||||
git checkout master
|
||||
git pull
|
||||
git merge --ff-only "$rel_branch"
|
||||
git merge "$rel_branch" --no-edit
|
||||
|
||||
# push everything to github
|
||||
git push origin master "$rel_branch" "$tag"
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
git push origin gh-pages
|
||||
# push master to github
|
||||
git push origin master
|
||||
|
||||
# finally, merge master back onto develop (if it exists)
|
||||
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
|
||||
git checkout develop
|
||||
git pull
|
||||
git merge master --no-edit
|
||||
|
||||
# When merging to develop, we need revert the `main` and `typings` fields if
|
||||
# we adjusted them previously.
|
||||
for i in main typings
|
||||
do
|
||||
# If a `lib` prefixed value is present, it means we adjusted the field
|
||||
# earlier at publish time, so we should revert it now.
|
||||
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
|
||||
# If there's a `src` prefixed value, use that, otherwise delete.
|
||||
# This is used to delete the `typings` field and reset `main` back
|
||||
# to the TypeScript source.
|
||||
src_value=$(jq -r ".matrix_src_$i" package.json)
|
||||
if [ "$src_value" != "null" ]; then
|
||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
else
|
||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$(git ls-files --modified package.json)" ]; then
|
||||
echo "Committing develop package.json"
|
||||
git commit package.json -m "Resetting package fields for development"
|
||||
fi
|
||||
|
||||
git push origin develop
|
||||
fi
|
||||
|
||||
# publish to npmjs
|
||||
npm publish
|
||||
|
||||
# finally, merge master back onto develop
|
||||
git checkout develop
|
||||
git pull
|
||||
git merge master
|
||||
git push origin develop
|
||||
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Outputs the body of the first entry of changelog file on stdin
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
found_first_header = False
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if re.match(r"^Changes in \[.*\]", line):
|
||||
if found_first_header:
|
||||
break
|
||||
found_first_header = True
|
||||
elif not re.match(r"^=+$", line) and len(line) > 0:
|
||||
print line
|
||||
@@ -0,0 +1,16 @@
|
||||
sonar.projectKey=matrix-js-sdk
|
||||
sonar.organization=matrix-org
|
||||
|
||||
# Encoding of the source code. Default is default system encoding
|
||||
#sonar.sourceEncoding=UTF-8
|
||||
|
||||
sonar.sources=src
|
||||
sonar.tests=spec
|
||||
sonar.exclusions=docs,examples,git-hooks
|
||||
|
||||
sonar.typescript.tsconfigPath=./tsconfig.json
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
sonar.coverage.exclusions=spec/**/*
|
||||
sonar.testExecutionReportPaths=coverage/test-report.xml
|
||||
|
||||
sonar.lang.patterns.ts=**/*.ts,**/*.tsx
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A mock implementation of the webstorage api
|
||||
* @constructor
|
||||
*/
|
||||
export function MockStorageApi() {
|
||||
this.data = {};
|
||||
this.keys = [];
|
||||
this.length = 0;
|
||||
}
|
||||
|
||||
MockStorageApi.prototype = {
|
||||
setItem: function(k, v) {
|
||||
this.data[k] = v;
|
||||
this._recalc();
|
||||
},
|
||||
getItem: function(k) {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
delete this.data[k];
|
||||
this._recalc();
|
||||
},
|
||||
key: function(index) {
|
||||
return this.keys[index];
|
||||
},
|
||||
_recalc: function() {
|
||||
const keys = [];
|
||||
for (const k in this.data) {
|
||||
if (!this.data.hasOwnProperty(k)) {
|
||||
continue;
|
||||
}
|
||||
keys.push(k);
|
||||
}
|
||||
this.keys = keys;
|
||||
this.length = keys.length;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import './olm-loader';
|
||||
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
|
||||
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
|
||||
import { logger } from '../src/logger';
|
||||
import { syncPromise } from "./test-utils/test-utils";
|
||||
import { createClient } from "../src/matrix";
|
||||
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
|
||||
import { MockStorageApi } from "./MockStorageApi";
|
||||
import { encodeUri } from "../src/utils";
|
||||
import { IDeviceKeys, IOneTimeKey } from "../src/crypto/dehydration";
|
||||
import { IKeyBackupSession } from "../src/crypto/keybackup";
|
||||
import { IHttpOpts } from "../src/http-api";
|
||||
import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client';
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*/
|
||||
export class TestClient {
|
||||
public readonly httpBackend: MockHttpBackend;
|
||||
public readonly client: MatrixClient;
|
||||
public deviceKeys: IDeviceKeys;
|
||||
public oneTimeKeys: Record<string, IOneTimeKey>;
|
||||
|
||||
constructor(
|
||||
public readonly userId?: string,
|
||||
public readonly deviceId?: string,
|
||||
accessToken?: string,
|
||||
sessionStoreBackend?: Storage,
|
||||
options?: Partial<ICreateClientOpts>,
|
||||
) {
|
||||
if (sessionStoreBackend === undefined) {
|
||||
sessionStoreBackend = new MockStorageApi();
|
||||
}
|
||||
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
|
||||
const fullOptions: ICreateClientOpts = {
|
||||
baseUrl: "http://" + userId + ".test.server",
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
request: this.httpBackend.requestFn as IHttpOpts["request"],
|
||||
...options,
|
||||
};
|
||||
if (!fullOptions.cryptoStore) {
|
||||
// expose this so the tests can get to it
|
||||
fullOptions.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
|
||||
}
|
||||
this.client = createClient(fullOptions);
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = {};
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return 'TestClient[' + this.userId + ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* start the client, and wait for it to initialise.
|
||||
*/
|
||||
public start(): Promise<void> {
|
||||
logger.log(this + ': starting');
|
||||
this.httpBackend.when("GET", "/versions").respond(200, {});
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
this.expectDeviceKeyUpload();
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// it will upload one-time keys.
|
||||
this.httpBackend.when("GET", "/sync").respond(200, { next_batch: 1 });
|
||||
|
||||
this.client.startClient({
|
||||
// set this so that we can get hold of failed events
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
this.httpBackend.flushAllExpected(),
|
||||
syncPromise(this.client),
|
||||
]).then(() => {
|
||||
logger.log(this + ': started');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* stop the client
|
||||
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
this.client.stopClient();
|
||||
await this.httpBackend.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will upload device keys.
|
||||
*/
|
||||
public expectDeviceKeyUpload() {
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content) => {
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
expect(content.device_keys).toBeTruthy();
|
||||
|
||||
logger.log(this + ': received device keys');
|
||||
// we expect this to happen before any one-time keys are uploaded.
|
||||
expect(Object.keys(this.oneTimeKeys).length).toEqual(0);
|
||||
|
||||
this.deviceKeys = content.device_keys;
|
||||
return { one_time_key_counts: { signed_curve25519: 0 } };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If one-time keys have already been uploaded, return them. Otherwise,
|
||||
* set up an expectation that the keys will be uploaded, and wait for
|
||||
* that to happen.
|
||||
*
|
||||
* @returns {Promise} for the one-time keys
|
||||
*/
|
||||
public awaitOneTimeKeyUpload(): Promise<Record<string, IOneTimeKey>> {
|
||||
if (Object.keys(this.oneTimeKeys).length != 0) {
|
||||
// already got one-time keys
|
||||
return Promise.resolve(this.oneTimeKeys);
|
||||
}
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
return { one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
} };
|
||||
});
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
.respond<IKeysUploadResponse, IUploadKeysRequest>(200, (_path, content: IUploadKeysRequest) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
logger.log('%s: received %i one-time keys', this,
|
||||
Object.keys(content.one_time_keys).length);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
return { one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
} };
|
||||
});
|
||||
|
||||
// this can take ages
|
||||
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
|
||||
expect(flushed).toEqual(2);
|
||||
return this.oneTimeKeys;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will query device keys.
|
||||
*
|
||||
* We check that the query contains each of the users in `response`.
|
||||
*
|
||||
* @param {Object} response response to the query.
|
||||
*/
|
||||
public expectKeyQuery(response: IDownloadKeyResult) {
|
||||
this.httpBackend.when('POST', '/keys/query').respond<IDownloadKeyResult>(
|
||||
200, (_path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect(content.device_keys[userId]).toEqual([]);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up expectations that the client will query key backups for a particular session
|
||||
*/
|
||||
public expectKeyBackupQuery(roomId: string, sessionId: string, status: number, response: IKeyBackupSession) {
|
||||
this.httpBackend.when('GET', encodeUri("/room_keys/keys/$roomId/$sessionId", {
|
||||
$roomId: roomId,
|
||||
$sessionId: sessionId,
|
||||
})).respond(status, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
public getDeviceKey(): string {
|
||||
const keyId = 'curve25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
public getSigningKey(): string {
|
||||
const keyId = 'ed25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[keyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* flush a single /sync request, and wait for the syncing event
|
||||
*/
|
||||
public flushSync(): Promise<void> {
|
||||
logger.log(`${this}: flushSync`);
|
||||
return Promise.all([
|
||||
this.httpBackend.flush('/sync', 1),
|
||||
syncPromise(this.client),
|
||||
]).then(() => {
|
||||
logger.log(`${this}: flushSync completed`);
|
||||
});
|
||||
}
|
||||
|
||||
public isFallbackICEServerAllowed(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public getUserId(): string {
|
||||
return this.userId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// stub for browser-matrix browserify tests
|
||||
global.XMLHttpRequest = jest.fn();
|
||||
|
||||
afterAll(() => {
|
||||
// clean up XMLHttpRequest mock
|
||||
global.XMLHttpRequest = undefined;
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// load XmlHttpRequest mock
|
||||
import "./setupTests";
|
||||
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
const USER_ID = "@user:test.server";
|
||||
const DEVICE_ID = "device_id";
|
||||
const ACCESS_TOKEN = "access_token";
|
||||
const ROOM_ID = "!room_id:server.test";
|
||||
|
||||
describe("Browserify Test", function() {
|
||||
let client;
|
||||
let httpBackend;
|
||||
|
||||
beforeEach(() => {
|
||||
const testClient = new TestClient(USER_ID, DEVICE_ID, ACCESS_TOKEN);
|
||||
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
client.stopClient();
|
||||
httpBackend.stop();
|
||||
});
|
||||
|
||||
it("Sync", function() {
|
||||
const event = utils.mkMembership({
|
||||
room: ROOM_ID,
|
||||
mship: "join",
|
||||
user: "@other_user:server.test",
|
||||
name: "Displayname",
|
||||
});
|
||||
|
||||
const syncData = {
|
||||
next_batch: "batch1",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [
|
||||
event,
|
||||
],
|
||||
limited: false,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
return Promise.race([
|
||||
httpBackend.flushAllExpected(),
|
||||
new Promise((_, reject) => {
|
||||
client.once("sync.unexpectedError", reject);
|
||||
}),
|
||||
]);
|
||||
}, 20000); // additional timeout as this test can take quite a while
|
||||
});
|
||||
@@ -0,0 +1,395 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestClient } from '../TestClient';
|
||||
import * as testUtils from '../test-utils/test-utils';
|
||||
import { logger } from '../../src/logger';
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
/**
|
||||
* get a /sync response which contains a single e2e room (ROOM_ID), with the
|
||||
* members given
|
||||
*
|
||||
* @param {string[]} roomMembers
|
||||
*
|
||||
* @return {object} sync response
|
||||
*/
|
||||
function getSyncResponse(roomMembers) {
|
||||
const stateEvents = [
|
||||
testUtils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
Array.prototype.push.apply(
|
||||
stateEvents,
|
||||
roomMembers.map(
|
||||
(m) => testUtils.mkMembership({
|
||||
mship: 'join',
|
||||
sender: m,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
state: {
|
||||
events: stateEvents,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
describe("DeviceList management:", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('not running deviceList tests: Olm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
let sessionStoreBackend;
|
||||
let aliceTestClient;
|
||||
|
||||
async function createTestClient() {
|
||||
const testClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend,
|
||||
);
|
||||
await testClient.client.initCrypto();
|
||||
return testClient;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
// we create our own sessionStoreBackend so that we can use it for
|
||||
// another TestClient.
|
||||
sessionStoreBackend = new testUtils.MockStorageApi();
|
||||
|
||||
aliceTestClient = await createTestClient();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
return aliceTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(function() {
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
logger.log("Forcing alice to download our device keys");
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(function() {
|
||||
logger.log("Telling alice to send a megolm message");
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/',
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
|
||||
// the crypto stuff can take a while, so give the requests a whole second.
|
||||
aliceTestClient.httpBackend.flushAllExpected({
|
||||
timeout: 1000,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip("We should not get confused by out-of-order device query responses", () => {
|
||||
// https://github.com/vector-im/element-web/issues/3126
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(() => {
|
||||
// to make sure the initial device queries are flushed out, we
|
||||
// attempt to send a message.
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
'@chris:abc': {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
|
||||
200, { event_id: '$event1' });
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
|
||||
() => aliceTestClient.httpBackend.flush('/send/', 1),
|
||||
),
|
||||
aliceTestClient.client.crypto.deviceList.saveIfDirty(),
|
||||
]);
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
expect(data.syncToken).toEqual(1);
|
||||
});
|
||||
|
||||
// invalidate bob's and chris's device lists in separate syncs
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '2',
|
||||
device_lists: {
|
||||
changed: ['@bob:xyz'],
|
||||
},
|
||||
});
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
|
||||
next_batch: '3',
|
||||
device_lists: {
|
||||
changed: ['@chris:abc'],
|
||||
},
|
||||
});
|
||||
// flush both syncs
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(() => {
|
||||
// check that we don't yet have a request for chris's devices.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@chris:abc': {},
|
||||
},
|
||||
token: '3',
|
||||
}).respond(200, {
|
||||
device_keys: { '@chris:abc': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
if (bobStat != 1 && bobStat != 2) {
|
||||
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
|
||||
bobStat);
|
||||
}
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// now add an expectation for a query for bob's devices, and let
|
||||
// it complete.
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query', {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
token: '2',
|
||||
}).respond(200, {
|
||||
device_keys: { '@bob:xyz': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
expect(bobStat).toEqual(3);
|
||||
const chrisStat = data.trackingStatus['@chris:abc'];
|
||||
if (chrisStat != 1 && chrisStat != 2) {
|
||||
throw new Error(
|
||||
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// now let the query for chris's devices complete.
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(1);
|
||||
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
const chrisStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(3);
|
||||
expect(chrisStat).toEqual(3);
|
||||
expect(data.syncToken).toEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/vector-im/element-web/issues/4983
|
||||
describe("Alice should know she has stale device lists", () => {
|
||||
beforeEach(async function() {
|
||||
await aliceTestClient.start();
|
||||
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz']));
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
},
|
||||
},
|
||||
);
|
||||
await aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toBeGreaterThan(
|
||||
0, "Alice should be tracking bob's device list",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Bob leaves", async function() {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ['@bob:xyz'],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: 'leave',
|
||||
sender: '@bob:xyz',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Alice leaves", async function() {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, {
|
||||
next_batch: 2,
|
||||
device_lists: {
|
||||
left: ['@bob:xyz'],
|
||||
},
|
||||
rooms: {
|
||||
leave: {
|
||||
[ROOM_ID]: {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: 'leave',
|
||||
sender: '@bob:xyz',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("when Bob leaves whilst Alice is offline", async function() {
|
||||
aliceTestClient.stop();
|
||||
|
||||
const anotherTestClient = await createTestClient();
|
||||
|
||||
try {
|
||||
await anotherTestClient.start();
|
||||
anotherTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse([]));
|
||||
await anotherTestClient.flushSync();
|
||||
await anotherTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
anotherTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
expect(bobStat).toEqual(
|
||||
0, "Alice should have marked bob's device list as untracked",
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
anotherTestClient.stop();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,249 +0,0 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
function MockStorageApi() {
|
||||
this.data = {};
|
||||
}
|
||||
MockStorageApi.prototype = {
|
||||
setItem: function(k, v) {
|
||||
this.data[k] = v;
|
||||
},
|
||||
getItem: function(k) {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
delete this.data[k];
|
||||
}
|
||||
};
|
||||
|
||||
describe("MatrixClient crypto", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var httpBackend;
|
||||
var aliClient;
|
||||
var roomId = "!room:localhost";
|
||||
var aliUserId = "@ali:localhost";
|
||||
var aliDeviceId = "zxcvb";
|
||||
var aliAccessToken = "aseukfgwef";
|
||||
var bobClient;
|
||||
var bobUserId = "@bob:localhost";
|
||||
var bobDeviceId = "bvcxz";
|
||||
var bobAccessToken = "fewgfkuesa";
|
||||
var bobOneTimeKeys;
|
||||
var bobDeviceKeys;
|
||||
var bobDeviceCurve25519Key;
|
||||
var bobDeviceEd25519Key;
|
||||
var aliLocalStore;
|
||||
var aliStorage;
|
||||
var bobStorage;
|
||||
var aliMessage;
|
||||
|
||||
beforeEach(function() {
|
||||
aliLocalStore = new MockStorageApi();
|
||||
aliStorage = new sdk.WebStorageSessionStore(aliLocalStore);
|
||||
bobStorage = new sdk.WebStorageSessionStore(new MockStorageApi());
|
||||
utils.beforeEach(this);
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
|
||||
aliClient = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: aliUserId,
|
||||
accessToken: aliAccessToken,
|
||||
deviceId: aliDeviceId,
|
||||
sessionStore: aliStorage
|
||||
});
|
||||
|
||||
bobClient = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: bobUserId,
|
||||
accessToken: bobAccessToken,
|
||||
deviceId: bobDeviceId,
|
||||
sessionStore: bobStorage
|
||||
});
|
||||
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
});
|
||||
|
||||
describe("Ali account setup", function() {
|
||||
it("should have device keys", function(done) {
|
||||
expect(aliClient.deviceKeys).toBeDefined();
|
||||
expect(aliClient.deviceKeys.user_id).toEqual(aliUserId);
|
||||
expect(aliClient.deviceKeys.device_id).toEqual(aliDeviceId);
|
||||
done();
|
||||
});
|
||||
it("should have a curve25519 key", function(done) {
|
||||
expect(aliClient.deviceCurve25519Key).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
function bobUploadsKeys(done) {
|
||||
var uploadPath = "/keys/upload/bvcxz";
|
||||
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).toEqual({});
|
||||
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
bobDeviceKeys = content.device_keys;
|
||||
bobOneTimeKeys = content.one_time_keys;
|
||||
var count = 0;
|
||||
for (var key in content.one_time_keys) {
|
||||
if (content.one_time_keys.hasOwnProperty(key)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
expect(count).toEqual(5);
|
||||
return {one_time_key_counts: {curve25519: count}};
|
||||
});
|
||||
return {one_time_key_counts: {}};
|
||||
});
|
||||
bobClient.uploadKeys(5);
|
||||
httpBackend.flush().done(function() {
|
||||
expect(bobDeviceKeys).toBeDefined();
|
||||
expect(bobOneTimeKeys).toBeDefined();
|
||||
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
|
||||
bobDeviceEd25519Key = bobDeviceKeys.keys["ed25519:bvcxz"];
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
it("Bob uploads without one-time keys and with one-time keys", bobUploadsKeys);
|
||||
|
||||
function aliDownloadsKeys(done) {
|
||||
var bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
httpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
expect(content.device_keys[bobUserId]).toEqual({});
|
||||
var result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
});
|
||||
aliClient.downloadKeys([bobUserId]).then(function() {
|
||||
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
|
||||
id: "bvcxz",
|
||||
key: bobDeviceEd25519Key
|
||||
}]);
|
||||
});
|
||||
httpBackend.flush().done(function() {
|
||||
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
|
||||
expect(devices).toEqual(bobKeys);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
it("Ali downloads Bobs keys", function(done) {
|
||||
bobUploadsKeys(function() {aliDownloadsKeys(done);});
|
||||
});
|
||||
|
||||
function aliEnablesEncryption(done) {
|
||||
httpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
|
||||
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
|
||||
for (var keyId in bobOneTimeKeys) {
|
||||
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf("curve25519:") === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var result = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
|
||||
return {one_time_keys: result};
|
||||
});
|
||||
aliClient.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
members: [aliUserId, bobUserId]
|
||||
}).then(function(res) {
|
||||
expect(res.missingUsers).toEqual([]);
|
||||
expect(res.missingDevices).toEqual({});
|
||||
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
httpBackend.flush();
|
||||
}
|
||||
|
||||
it("Ali enables encryption", function(done) {
|
||||
bobUploadsKeys(function() {
|
||||
aliDownloadsKeys(function() {
|
||||
aliEnablesEncryption(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function aliSendsMessage(done) {
|
||||
var txnId = "a.transaction.id";
|
||||
var path = "/send/m.room.encrypted/" + txnId;
|
||||
httpBackend.when("PUT", path).respond(200, function(path, content) {
|
||||
aliMessage = content;
|
||||
expect(aliMessage.ciphertext[bobDeviceCurve25519Key]).toBeDefined();
|
||||
return {};
|
||||
});
|
||||
aliClient.sendMessage(
|
||||
roomId, {msgtype: "m.text", body: "Hello, World"}, txnId
|
||||
);
|
||||
httpBackend.flush().done(function() {done();});
|
||||
}
|
||||
|
||||
it("Ali sends a message", function(done) {
|
||||
bobUploadsKeys(function() {
|
||||
aliDownloadsKeys(function() {
|
||||
aliEnablesEncryption(function() {
|
||||
aliSendsMessage(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function bobRecvMessage(done) {
|
||||
var syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: aliMessage
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
bobClient.on("event", function(event) {
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toEqual({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World"
|
||||
});
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
bobClient.startClient();
|
||||
httpBackend.flush();
|
||||
}
|
||||
|
||||
it("Bob receives a message", function(done) {
|
||||
bobUploadsKeys(function() {
|
||||
aliDownloadsKeys(function() {
|
||||
aliEnablesEncryption(function() {
|
||||
aliSendsMessage(function() {
|
||||
bobRecvMessage(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 30000); //timeout after 30s
|
||||
|
||||
});
|
||||
@@ -0,0 +1,676 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* This file consists of a set of integration tests which try to simulate
|
||||
* communication via an Olm-encrypted room between two users, Alice and Bob.
|
||||
*
|
||||
* Note that megolm (group) conversation is not tested here.
|
||||
*
|
||||
* See also `megolm.spec.js`.
|
||||
*/
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import '../olm-loader';
|
||||
|
||||
import { logger } from '../../src/logger';
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
|
||||
|
||||
let aliTestClient: TestClient;
|
||||
const roomId = "!room:localhost";
|
||||
const aliUserId = "@ali:localhost";
|
||||
const aliDeviceId = "zxcvb";
|
||||
const aliAccessToken = "aseukfgwef";
|
||||
let bobTestClient: TestClient;
|
||||
const bobUserId = "@bob:localhost";
|
||||
const bobDeviceId = "bvcxz";
|
||||
const bobAccessToken = "fewgfkuesa";
|
||||
let aliMessages: IContent[];
|
||||
let bobMessages: IContent[];
|
||||
|
||||
// IMessage isn't exported by src/crypto/algorithms/olm.ts
|
||||
interface OlmPayload {
|
||||
type: number;
|
||||
body: string;
|
||||
}
|
||||
|
||||
async function bobUploadsDeviceKeys(): Promise<void> {
|
||||
bobTestClient.expectDeviceKeyUpload();
|
||||
await Promise.all([
|
||||
bobTestClient.client.uploadKeys(),
|
||||
bobTestClient.httpBackend.flushAllExpected(),
|
||||
]);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that querier will query uploader's keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
*/
|
||||
function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise<number> {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(uploader.deviceKeys).toBeTruthy();
|
||||
|
||||
const uploaderKeys = {};
|
||||
uploaderKeys[uploader.deviceId] = uploader.deviceKeys;
|
||||
querier.httpBackend.when("POST", "/keys/query")
|
||||
.respond(200, function(_path, content) {
|
||||
expect(content.device_keys[uploader.userId]).toEqual([]);
|
||||
const result = {};
|
||||
result[uploader.userId] = uploaderKeys;
|
||||
return { device_keys: result };
|
||||
});
|
||||
return querier.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
const expectAliQueryKeys = () => expectQueryKeys(aliTestClient, bobTestClient);
|
||||
const expectBobQueryKeys = () => expectQueryKeys(bobTestClient, aliTestClient);
|
||||
|
||||
/**
|
||||
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
|
||||
*
|
||||
* @return {promise} resolves once the http request has completed.
|
||||
*/
|
||||
async function expectAliClaimKeys(): Promise<void> {
|
||||
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/claim",
|
||||
).respond(200, function(_path, content) {
|
||||
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
let keyId = null;
|
||||
for (keyId in keys) {
|
||||
if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf(claimType + ":") === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = {};
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
|
||||
return { one_time_keys: result };
|
||||
});
|
||||
// it can take a while to process the key query, so give it some extra
|
||||
// time, and make sure the claim actually happens rather than ploughing on
|
||||
// confusingly.
|
||||
const r = await aliTestClient.httpBackend.flush("/keys/claim", 1, 500);
|
||||
expect(r).toEqual(1);
|
||||
}
|
||||
|
||||
async function aliDownloadsKeys(): Promise<void> {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobTestClient.getSigningKey()).toBeTruthy();
|
||||
|
||||
const p1 = async () => {
|
||||
await aliTestClient.client.downloadKeys([bobUserId]);
|
||||
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
expect(devices.length).toEqual(1);
|
||||
expect(devices[0].deviceId).toEqual("bvcxz");
|
||||
};
|
||||
const p2 = expectAliQueryKeys;
|
||||
|
||||
// check that the localStorage is updated as we expect (not sure this is
|
||||
// an integration test, but meh)
|
||||
await Promise.all([p1(), p2()]);
|
||||
await aliTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
// @ts-ignore - protected
|
||||
aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const devices = data.devices[bobUserId];
|
||||
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
|
||||
expect(devices[bobDeviceId].verified).
|
||||
toBe(0); // DeviceVerification.UNVERIFIED
|
||||
});
|
||||
}
|
||||
|
||||
async function clientEnablesEncryption(client: MatrixClient): Promise<void> {
|
||||
await client.setRoomEncryption(roomId, {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
});
|
||||
expect(client.isRoomEncrypted(roomId)).toBeTruthy();
|
||||
}
|
||||
const aliEnablesEncryption = () => clientEnablesEncryption(aliTestClient.client);
|
||||
const bobEnablesEncryption = () => clientEnablesEncryption(bobTestClient.client);
|
||||
|
||||
/**
|
||||
* Ali sends a message, first claiming e2e keys. Set the expectations and
|
||||
* check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function aliSendsFirstMessage(): Promise<OlmPayload> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, ciphertext] = await Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliQueryKeys()
|
||||
.then(expectAliClaimKeys)
|
||||
.then(expectAliSendMessageRequest),
|
||||
]);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ali sends a message without first claiming e2e keys. Set the expectations
|
||||
* and check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function aliSendsMessage(): Promise<OlmPayload> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, ciphertext] = await Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliSendMessageRequest(),
|
||||
]);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
|
||||
* expectations and check the results.
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Ali's device.
|
||||
*/
|
||||
async function bobSendsReplyMessage(): Promise<OlmPayload> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, ciphertext] = await Promise.all([
|
||||
sendMessage(bobTestClient.client),
|
||||
expectBobQueryKeys()
|
||||
.then(expectBobSendMessageRequest),
|
||||
]);
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Ali will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function expectAliSendMessageRequest(): Promise<OlmPayload> {
|
||||
const content = await expectSendMessageRequest(aliTestClient.httpBackend);
|
||||
aliMessages.push(content);
|
||||
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
|
||||
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an expectation that Bob will send a message, and flush the request
|
||||
*
|
||||
* @return {promise} which resolves to the ciphertext for Bob's device.
|
||||
*/
|
||||
async function expectBobSendMessageRequest(): Promise<OlmPayload> {
|
||||
const content = await expectSendMessageRequest(bobTestClient.httpBackend);
|
||||
bobMessages.push(content);
|
||||
const aliKeyId = "curve25519:" + aliDeviceId;
|
||||
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
|
||||
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
||||
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
function sendMessage(client: MatrixClient): Promise<ISendEventResponse> {
|
||||
return client.sendMessage(
|
||||
roomId, { msgtype: "m.text", body: "Hello, World" },
|
||||
);
|
||||
}
|
||||
|
||||
async function expectSendMessageRequest(httpBackend: TestClient["httpBackend"]): Promise<IContent> {
|
||||
const path = "/send/m.room.encrypted/";
|
||||
const prom = new Promise((resolve) => {
|
||||
httpBackend.when("PUT", path).respond(200, function(_path, content) {
|
||||
resolve(content);
|
||||
return {
|
||||
event_id: "asdfgh",
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// it can take a while to process the key query
|
||||
await httpBackend.flush(path, 1);
|
||||
return prom;
|
||||
}
|
||||
|
||||
function aliRecvMessage(): Promise<void> {
|
||||
const message = bobMessages.shift();
|
||||
return recvMessage(
|
||||
aliTestClient.httpBackend, aliTestClient.client, bobUserId, message,
|
||||
);
|
||||
}
|
||||
|
||||
function bobRecvMessage(): Promise<void> {
|
||||
const message = aliMessages.shift();
|
||||
return recvMessage(
|
||||
bobTestClient.httpBackend, bobTestClient.client, aliUserId, message,
|
||||
);
|
||||
}
|
||||
|
||||
async function recvMessage(
|
||||
httpBackend: TestClient["httpBackend"],
|
||||
client: MatrixClient,
|
||||
sender: string,
|
||||
message: IContent,
|
||||
): Promise<void> {
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: sender,
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const eventPromise = new Promise<MatrixEvent>((resolve) => {
|
||||
const onEvent = function(event: MatrixEvent) {
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
logger.log(client.credentials.userId + " received event",
|
||||
event);
|
||||
|
||||
client.removeListener(ClientEvent.Event, onEvent);
|
||||
resolve(event);
|
||||
};
|
||||
client.on(ClientEvent.Event, onEvent);
|
||||
});
|
||||
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
const preDecryptionEvent = await eventPromise;
|
||||
expect(preDecryptionEvent.isEncrypted()).toBeTruthy();
|
||||
// it may still be being decrypted
|
||||
const event = await testUtils.awaitDecryption(preDecryptionEvent);
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toMatchObject({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World",
|
||||
});
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an initial sync response to the client (which just includes the member
|
||||
* list for our test room).
|
||||
*
|
||||
* @param {TestClient} testClient
|
||||
* @returns {Promise} which resolves when the sync has been flushed.
|
||||
*/
|
||||
function firstSync(testClient: TestClient): Promise<void> {
|
||||
// send a sync response including our test room.
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: { },
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
user: aliUserId,
|
||||
}),
|
||||
testUtils.mkMembership({
|
||||
mship: "join",
|
||||
user: bobUserId,
|
||||
}),
|
||||
],
|
||||
},
|
||||
timeline: {
|
||||
events: [],
|
||||
},
|
||||
};
|
||||
|
||||
testClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
return testClient.flushSync();
|
||||
}
|
||||
|
||||
describe("MatrixClient crypto", () => {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
|
||||
await aliTestClient.client.initCrypto();
|
||||
|
||||
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
|
||||
await bobTestClient.client.initCrypto();
|
||||
|
||||
aliMessages = [];
|
||||
bobMessages = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
|
||||
|
||||
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
|
||||
});
|
||||
|
||||
it("Bob uploads device keys", bobUploadsDeviceKeys);
|
||||
|
||||
it("Ali downloads Bobs device keys", async () => {
|
||||
await bobUploadsDeviceKeys();
|
||||
await aliDownloadsKeys();
|
||||
});
|
||||
|
||||
it("Ali gets keys with an invalid signature", async () => {
|
||||
await bobUploadsDeviceKeys();
|
||||
// tamper bob's keys
|
||||
const bobDeviceKeys = bobTestClient.deviceKeys;
|
||||
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
|
||||
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
|
||||
await Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
expectAliQueryKeys(),
|
||||
]);
|
||||
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect userId", async () => {
|
||||
const eveUserId = "@eve:localhost";
|
||||
|
||||
const bobDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'bvcxz',
|
||||
keys: {
|
||||
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
|
||||
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
|
||||
},
|
||||
user_id: '@eve:localhost',
|
||||
signatures: {
|
||||
'@eve:localhost': {
|
||||
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
|
||||
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||
|
||||
await Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
|
||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]);
|
||||
const [bobDevices, eveDevices] = await Promise.all([
|
||||
aliTestClient.client.getStoredDevicesForUser(bobUserId),
|
||||
aliTestClient.client.getStoredDevicesForUser(eveUserId),
|
||||
]);
|
||||
// should get an empty list
|
||||
expect(bobDevices).toEqual([]);
|
||||
expect(eveDevices).toEqual([]);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect deviceId", async () => {
|
||||
const bobDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'bad_device',
|
||||
keys: {
|
||||
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
|
||||
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
|
||||
},
|
||||
user_id: '@bob:localhost',
|
||||
signatures: {
|
||||
'@bob:localhost': {
|
||||
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
|
||||
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliTestClient.httpBackend.when(
|
||||
"POST", "/keys/query",
|
||||
).respond(200, { device_keys: { [bobUserId]: bobKeys } });
|
||||
|
||||
await Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]);
|
||||
const devices = aliTestClient.client.getStoredDevicesForUser(bobUserId);
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
});
|
||||
|
||||
it("Bob starts his client and uploads device keys and one-time keys", async () => {
|
||||
await bobTestClient.start();
|
||||
const keys = await bobTestClient.awaitOneTimeKeyUpload();
|
||||
expect(Object.keys(keys).length).toEqual(5);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||
});
|
||||
|
||||
it("Ali sends a message", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
});
|
||||
|
||||
it("Bob receives a message", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
await bobRecvMessage();
|
||||
});
|
||||
|
||||
it("Bob receives a message with a bogus sender", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
const message = aliMessages.shift();
|
||||
const syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: "@bogus:sender",
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const eventPromise = new Promise<MatrixEvent>((resolve) => {
|
||||
const onEvent = function(event: MatrixEvent) {
|
||||
logger.log(bobUserId + " received event", event);
|
||||
resolve(event);
|
||||
};
|
||||
bobTestClient.client.once(ClientEvent.Event, onEvent);
|
||||
});
|
||||
await bobTestClient.httpBackend.flushAllExpected();
|
||||
const preDecryptionEvent = await eventPromise;
|
||||
expect(preDecryptionEvent.isEncrypted()).toBeTruthy();
|
||||
// it may still be being decrypted
|
||||
const event = await testUtils.awaitDecryption(preDecryptionEvent);
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
});
|
||||
|
||||
it("Ali blocks Bob's device", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliDownloadsKeys();
|
||||
aliTestClient.client.setDeviceBlocked(bobUserId, bobDeviceId, true);
|
||||
const p1 = sendMessage(aliTestClient.client);
|
||||
const p2 = expectSendMessageRequest(aliTestClient.httpBackend)
|
||||
.then(function(sentContent) {
|
||||
// no unblocked devices, so the ciphertext should be empty
|
||||
expect(sentContent.ciphertext).toEqual({});
|
||||
});
|
||||
await Promise.all([p1, p2]);
|
||||
});
|
||||
|
||||
it("Bob receives two pre-key messages", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
await bobRecvMessage();
|
||||
await aliSendsMessage();
|
||||
await bobRecvMessage();
|
||||
});
|
||||
|
||||
it("Bob replies to the message", async () => {
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await bobTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
await firstSync(bobTestClient);
|
||||
await aliEnablesEncryption();
|
||||
await aliSendsFirstMessage();
|
||||
await bobRecvMessage();
|
||||
await bobEnablesEncryption();
|
||||
const ciphertext = await bobSendsReplyMessage();
|
||||
expect(ciphertext.type).toEqual(1);
|
||||
await aliRecvMessage();
|
||||
});
|
||||
|
||||
it("Ali does a key query when encryption is enabled", async () => {
|
||||
// enabling encryption in the room should make alice download devices
|
||||
// for both members.
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} }, failures: {} });
|
||||
await aliTestClient.start();
|
||||
await firstSync(aliTestClient);
|
||||
const syncData = {
|
||||
next_batch: '2',
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
state: {
|
||||
events: [
|
||||
testUtils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
aliTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, syncData);
|
||||
await aliTestClient.httpBackend.flush('/sync', 1);
|
||||
aliTestClient.expectKeyQuery({
|
||||
device_keys: {
|
||||
[bobUserId]: {},
|
||||
},
|
||||
failures: {},
|
||||
});
|
||||
await aliTestClient.httpBackend.flushAllExpected();
|
||||
});
|
||||
|
||||
it("Upload new oneTimeKeys based on a /sync request - no count-asking", async () => {
|
||||
// Send a response which causes a key upload
|
||||
const httpBackend = aliTestClient.httpBackend;
|
||||
const syncDataEmpty = {
|
||||
next_batch: "a",
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// enqueue expectations:
|
||||
// * Sync with empty one_time_keys => upload keys
|
||||
|
||||
logger.log(aliTestClient + ': starting');
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
aliTestClient.expectDeviceKeyUpload();
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// it will upload one-time keys.
|
||||
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
|
||||
|
||||
await Promise.all([
|
||||
aliTestClient.client.startClient({}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
logger.log(aliTestClient + ': started');
|
||||
httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (_path, content) => {
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
expect(Object.keys(content.one_time_keys).length).toBeGreaterThanOrEqual(1);
|
||||
logger.log('received %i one-time keys', Object.keys(content.one_time_keys).length);
|
||||
// cancel futher calls by telling the client
|
||||
// we have more than we need
|
||||
return {
|
||||
one_time_key_counts: {
|
||||
signed_curve25519: 70,
|
||||
},
|
||||
};
|
||||
});
|
||||
await httpBackend.flushAllExpected();
|
||||
});
|
||||
});
|
||||
@@ -1,23 +1,17 @@
|
||||
"use strict";
|
||||
var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
var client, httpBackend;
|
||||
var selfUserId = "@alice:localhost";
|
||||
var selfAccessToken = "aseukfgwef";
|
||||
let client;
|
||||
let httpBackend;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: selfUserId,
|
||||
accessToken: selfAccessToken
|
||||
});
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
httpBackend.when("GET", "/versions").respond(200, {});
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
});
|
||||
@@ -25,17 +19,18 @@ describe("MatrixClient events", function() {
|
||||
afterEach(function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
client.stopClient();
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
describe("emissions", function() {
|
||||
var SYNC_DATA = {
|
||||
const SYNC_DATA = {
|
||||
next_batch: "s_5_3",
|
||||
presence: {
|
||||
events: [
|
||||
utils.mkPresence({
|
||||
user: "@foo:bar", name: "Foo Bar", presence: "online"
|
||||
})
|
||||
]
|
||||
user: "@foo:bar", name: "Foo Bar", presence: "online",
|
||||
}),
|
||||
],
|
||||
},
|
||||
rooms: {
|
||||
join: {
|
||||
@@ -43,30 +38,30 @@ describe("MatrixClient events", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm"
|
||||
})
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm",
|
||||
}),
|
||||
],
|
||||
prev_batch: "s"
|
||||
prev_batch: "s",
|
||||
},
|
||||
state: {
|
||||
events: [
|
||||
utils.mkMembership({
|
||||
room: "!erufh:bar", mship: "join", user: "@foo:bar"
|
||||
room: "!erufh:bar", mship: "join", user: "@foo:bar",
|
||||
}),
|
||||
utils.mkEvent({
|
||||
type: "m.room.create", room: "!erufh:bar",
|
||||
user: "@foo:bar",
|
||||
content: {
|
||||
creator: "@foo:bar"
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
creator: "@foo:bar",
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
var NEXT_SYNC_DATA = {
|
||||
const NEXT_SYNC_DATA = {
|
||||
next_batch: "e_6_7",
|
||||
rooms: {
|
||||
join: {
|
||||
@@ -74,44 +69,45 @@ describe("MatrixClient events", function() {
|
||||
timeline: {
|
||||
events: [
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: "ello ello"
|
||||
room: "!erufh:bar", user: "@foo:bar",
|
||||
msg: "ello ello",
|
||||
}),
|
||||
utils.mkMessage({
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: ":D"
|
||||
room: "!erufh:bar", user: "@foo:bar", msg: ":D",
|
||||
}),
|
||||
]
|
||||
],
|
||||
},
|
||||
ephemeral: {
|
||||
events: [
|
||||
utils.mkEvent({
|
||||
type: "m.typing", room: "!erufh:bar", content: {
|
||||
user_ids: ["@foo:bar"]
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
user_ids: ["@foo:bar"],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("should emit events from both the first and subsequent /sync calls",
|
||||
function(done) {
|
||||
function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
var expectedEvents = [];
|
||||
let expectedEvents = [];
|
||||
expectedEvents = expectedEvents.concat(
|
||||
SYNC_DATA.presence.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events
|
||||
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
|
||||
);
|
||||
|
||||
client.on("event", function(event) {
|
||||
var found = false;
|
||||
for (var i = 0; i < expectedEvents.length; i++) {
|
||||
let found = false;
|
||||
for (let i = 0; i < expectedEvents.length; i++) {
|
||||
if (expectedEvents[i].event_id === event.getId()) {
|
||||
expectedEvents.splice(i, 1);
|
||||
found = true;
|
||||
@@ -119,49 +115,56 @@ describe("MatrixClient events", function() {
|
||||
}
|
||||
}
|
||||
expect(found).toBe(
|
||||
true, "Unexpected 'event' emitted: " + event.getType()
|
||||
true, "Unexpected 'event' emitted: " + event.getType(),
|
||||
);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
return Promise.all([
|
||||
// wait for two SYNCING events
|
||||
utils.syncPromise(client).then(() => {
|
||||
return utils.syncPromise(client);
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]).then(() => {
|
||||
expect(expectedEvents.length).toEqual(
|
||||
0, "Failed to see all events from /sync calls"
|
||||
0, "Failed to see all events from /sync calls",
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit User events", function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
var fired = false;
|
||||
let fired = false;
|
||||
client.on("User.presence", function(event, user) {
|
||||
fired = true;
|
||||
expect(user).toBeDefined();
|
||||
expect(event).toBeDefined();
|
||||
if (!user || !event) { return; }
|
||||
expect(user).toBeTruthy();
|
||||
expect(event).toBeTruthy();
|
||||
if (!user || !event) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
|
||||
expect(event.event).toMatch(SYNC_DATA.presence.events[0]);
|
||||
expect(user.presence).toEqual(
|
||||
SYNC_DATA.presence.events[0].content.presence
|
||||
SYNC_DATA.presence.events[0].content.presence,
|
||||
);
|
||||
});
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
httpBackend.flushAllExpected().then(function() {
|
||||
expect(fired).toBe(true, "User.presence didn't fire.");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Room events", function(done) {
|
||||
it("should emit Room events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
var roomInvokeCount = 0;
|
||||
var roomNameInvokeCount = 0;
|
||||
var timelineFireCount = 0;
|
||||
let roomInvokeCount = 0;
|
||||
let roomNameInvokeCount = 0;
|
||||
let timelineFireCount = 0;
|
||||
client.on("Room", function(room) {
|
||||
roomInvokeCount++;
|
||||
expect(room.roomId).toEqual("!erufh:bar");
|
||||
@@ -176,35 +179,37 @@ describe("MatrixClient events", function() {
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
]).then(function() {
|
||||
expect(roomInvokeCount).toEqual(
|
||||
1, "Room fired wrong number of times."
|
||||
1, "Room fired wrong number of times.",
|
||||
);
|
||||
expect(roomNameInvokeCount).toEqual(
|
||||
1, "Room.name fired wrong number of times."
|
||||
1, "Room.name fired wrong number of times.",
|
||||
);
|
||||
expect(timelineFireCount).toEqual(
|
||||
3, "Room.timeline fired the wrong number of times"
|
||||
3, "Room.timeline fired the wrong number of times",
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomState events", function(done) {
|
||||
it("should emit RoomState events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
var roomStateEventTypes = [
|
||||
"m.room.member", "m.room.create"
|
||||
const roomStateEventTypes = [
|
||||
"m.room.member", "m.room.create",
|
||||
];
|
||||
var eventsInvokeCount = 0;
|
||||
var membersInvokeCount = 0;
|
||||
var newMemberInvokeCount = 0;
|
||||
let eventsInvokeCount = 0;
|
||||
let membersInvokeCount = 0;
|
||||
let newMemberInvokeCount = 0;
|
||||
client.on("RoomState.events", function(event, state) {
|
||||
eventsInvokeCount++;
|
||||
var index = roomStateEventTypes.indexOf(event.getType());
|
||||
const index = roomStateEventTypes.indexOf(event.getType());
|
||||
expect(index).not.toEqual(
|
||||
-1, "Unexpected room state event type: " + event.getType()
|
||||
-1, "Unexpected room state event type: " + event.getType(),
|
||||
);
|
||||
if (index >= 0) {
|
||||
roomStateEventTypes.splice(index, 1);
|
||||
@@ -225,28 +230,30 @@ describe("MatrixClient events", function() {
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
]).then(function() {
|
||||
expect(membersInvokeCount).toEqual(
|
||||
1, "RoomState.members fired wrong number of times"
|
||||
1, "RoomState.members fired wrong number of times",
|
||||
);
|
||||
expect(newMemberInvokeCount).toEqual(
|
||||
1, "RoomState.newMember fired wrong number of times"
|
||||
1, "RoomState.newMember fired wrong number of times",
|
||||
);
|
||||
expect(eventsInvokeCount).toEqual(
|
||||
2, "RoomState.events fired wrong number of times"
|
||||
2, "RoomState.events fired wrong number of times",
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit RoomMember events", function(done) {
|
||||
it("should emit RoomMember events", function() {
|
||||
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
|
||||
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
|
||||
|
||||
var typingInvokeCount = 0;
|
||||
var powerLevelInvokeCount = 0;
|
||||
var nameInvokeCount = 0;
|
||||
var membershipInvokeCount = 0;
|
||||
let typingInvokeCount = 0;
|
||||
let powerLevelInvokeCount = 0;
|
||||
let nameInvokeCount = 0;
|
||||
let membershipInvokeCount = 0;
|
||||
client.on("RoomMember.name", function(event, member) {
|
||||
nameInvokeCount++;
|
||||
});
|
||||
@@ -264,40 +271,61 @@ describe("MatrixClient events", function() {
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client, 2),
|
||||
]).then(function() {
|
||||
expect(typingInvokeCount).toEqual(
|
||||
1, "RoomMember.typing fired wrong number of times"
|
||||
1, "RoomMember.typing fired wrong number of times",
|
||||
);
|
||||
expect(powerLevelInvokeCount).toEqual(
|
||||
0, "RoomMember.powerLevel fired wrong number of times"
|
||||
0, "RoomMember.powerLevel fired wrong number of times",
|
||||
);
|
||||
expect(nameInvokeCount).toEqual(
|
||||
0, "RoomMember.name fired wrong number of times"
|
||||
0, "RoomMember.name fired wrong number of times",
|
||||
);
|
||||
expect(membershipInvokeCount).toEqual(
|
||||
1, "RoomMember.membership fired wrong number of times"
|
||||
1, "RoomMember.membership fired wrong number of times",
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function(done) {
|
||||
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' });
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
|
||||
const error = { errcode: 'M_UNKNOWN_TOKEN' };
|
||||
httpBackend.when("GET", "/sync").respond(401, error);
|
||||
|
||||
var sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(event, member) {
|
||||
let sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
return httpBackend.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(
|
||||
1, "Session.logged_out fired wrong number of times"
|
||||
1, "Session.logged_out fired wrong number of times",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
|
||||
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
|
||||
httpBackend.when("GET", "/sync").respond(401, error);
|
||||
|
||||
let sessionLoggedOutCount = 0;
|
||||
client.on("Session.logged_out", function(errObj) {
|
||||
sessionLoggedOutCount++;
|
||||
expect(errObj.data).toEqual(error);
|
||||
});
|
||||
|
||||
client.startClient();
|
||||
|
||||
return httpBackend.flushAllExpected().then(function() {
|
||||
expect(sessionLoggedOutCount).toEqual(
|
||||
1, "Session.logged_out fired wrong number of times",
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user