Compare commits
756 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 467b75f2dc | |||
| 1f728cab92 | |||
| 68c1171294 | |||
| f4f01913fe | |||
| 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 | |||
| 36e2533164 | |||
| 9084b4e7aa | |||
| f724da7e84 | |||
| 4b8f47e2b4 | |||
| 35ecbed29d | |||
| 30ed153ad1 | |||
| a1098989ff | |||
| 42bb63e07d | |||
| 683aae5ed5 | |||
| 49ef274a83 | |||
| af16bba1ec | |||
| 781086608e | |||
| e9c98b03b0 | |||
| 75370f7855 | |||
| 4a79e13410 | |||
| 220061f022 | |||
| 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 | |||
| 8f9a682d34 | |||
| 102d5acf09 | |||
| e2ec8952e3 | |||
| b4eff9b996 | |||
| d050261fa9 | |||
| d29330815e | |||
| 801b4022de | |||
| e78fbd1dff | |||
| c543358826 | |||
| ff2954839b | |||
| 1a91b88968 | |||
| 25ec81b6c7 | |||
| a50802a63f | |||
| ce1d374a82 | |||
| 6dca8ac460 | |||
| 4dc21674d5 | |||
| 8805dd8c01 | |||
| 73b624e761 | |||
| c44fd972b6 | |||
| ff344bc110 | |||
| b0e2a38325 | |||
| fbb741ab10 | |||
| 3a3be36f4c | |||
| cb91c4292c | |||
| 80722ce145 | |||
| 07bfa5532e | |||
| cea1a3ff91 | |||
| 270be2df7a | |||
| e73b969066 | |||
| 98e2154f0f | |||
| 2c0549a772 | |||
| acb9bc8cc5 | |||
| 6f44aa39e8 | |||
| c706618229 | |||
| c26b571b1c | |||
| cb3075b084 | |||
| 66fb21bd0b | |||
| 498109bd53 | |||
| 6711ab5f9a | |||
| ac8fa58845 | |||
| 095f656998 | |||
| 9e30c4f7dd | |||
| 56a2eeac77 | |||
| d104f2f4a7 | |||
| e8367ad241 | |||
| f17cd142d5 | |||
| e8edc554a6 | |||
| bd8aca83ac | |||
| 7ebc1cfac5 | |||
| 2422204d6a | |||
| 6c98c3c662 | |||
| 1d1310f034 | |||
| 841888c480 | |||
| 52c031f160 | |||
| 155113b75d | |||
| 2a863025c6 | |||
| b6763ce89f | |||
| f346cd6b8d | |||
| 0c47412c75 | |||
| ea1ef3dbec | |||
| 61fd62ff81 | |||
| 32197ea903 | |||
| 65de184d88 | |||
| 52764045ce | |||
| 4b0db6c472 | |||
| 3ec89a89df | |||
| 9e6b72bf38 | |||
| 747723c8fb | |||
| 40cd4629db | |||
| 52a893a811 | |||
| 7cc94e1e1e | |||
| 88945a6d6d | |||
| 7203c5aaf0 | |||
| c305058c46 | |||
| 91bbf7d1a8 | |||
| 2d5857f145 | |||
| 91af9a411d | |||
| 9a9ed124f5 | |||
| 4e5442d972 | |||
| b5fa54f91e | |||
| 2246aede4b | |||
| 4b2d409c69 | |||
| 684511ff06 | |||
| 88b328df7e | |||
| e3583dd04e | |||
| e484a2ebb5 | |||
| 5caf05cfa1 | |||
| 72f86258d0 | |||
| 874cb3b779 | |||
| f21e0228b4 | |||
| 01cd82bc6a | |||
| d2a6a8b283 | |||
| f242e460ed | |||
| 95e08253a6 | |||
| 202a4fa6f1 | |||
| 576f46cb88 | |||
| 2d73805ca3 | |||
| eedfa550a6 | |||
| 4ca718adc4 | |||
| 7c4ced8f46 | |||
| 7018f4ab25 | |||
| fda13875ef | |||
| 8005917452 | |||
| 91eee8587e | |||
| e9132abc25 | |||
| 444eac5c6e | |||
| 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 | |||
| 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 | |||
| 4cc4b28c47 | |||
| fad9b4c67f | |||
| ade3b3a021 | |||
| d8c4101fdd | |||
| 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 | |||
| 7529d2b638 | |||
| c8a18d51e6 | |||
| d77af1e67a | |||
| 81c95224d1 | |||
| 22713d8f89 | |||
| 6ec7e3a0b7 | |||
| 1a1fe759c3 | |||
| a919c798f8 | |||
| 80fe66c481 | |||
| 4577dc8f44 | |||
| cf0a5305e0 | |||
| e69e6e1981 | |||
| 0febd99fbe |
+22
-7
@@ -1,13 +1,14 @@
|
||||
module.exports = {
|
||||
extends: ["matrix-org"],
|
||||
plugins: [
|
||||
"babel",
|
||||
"matrix-org",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/babel",
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
"no-var": ["warn"],
|
||||
"prefer-rest-params": ["warn"],
|
||||
@@ -32,12 +33,26 @@ module.exports = {
|
||||
"no-console": "error",
|
||||
},
|
||||
overrides: [{
|
||||
"files": ["src/**/*.ts"],
|
||||
"extends": ["matrix-org/ts"],
|
||||
"rules": {
|
||||
// While we're converting to ts we make heavy use of this
|
||||
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",
|
||||
|
||||
"quotes": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ lib-cov
|
||||
out
|
||||
/dist
|
||||
/lib
|
||||
/specbuild
|
||||
|
||||
# version file and tarball created by `npm pack` / `yarn pack`
|
||||
/git-revision.txt
|
||||
|
||||
+594
@@ -1,3 +1,597 @@
|
||||
Changes in [12.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.0.0) (2021-06-21)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v12.0.0-rc.1...v12.0.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [12.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.0.0-rc.1) (2021-06-15)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.2.0...v12.0.0-rc.1)
|
||||
|
||||
* Rework how disambiguation is handled
|
||||
[\#1730](https://github.com/matrix-org/matrix-js-sdk/pull/1730)
|
||||
* Fix baseToString for n=0 edge case to match inverse stringToBase
|
||||
[\#1735](https://github.com/matrix-org/matrix-js-sdk/pull/1735)
|
||||
* Move various types from the react-sdk to the js-sdk
|
||||
[\#1734](https://github.com/matrix-org/matrix-js-sdk/pull/1734)
|
||||
* Unstable implementation of MSC3089: File Trees
|
||||
[\#1732](https://github.com/matrix-org/matrix-js-sdk/pull/1732)
|
||||
* Add MSC3230 event type to enum
|
||||
[\#1729](https://github.com/matrix-org/matrix-js-sdk/pull/1729)
|
||||
* Add separate reason code for transferred calls
|
||||
[\#1731](https://github.com/matrix-org/matrix-js-sdk/pull/1731)
|
||||
* Use sendonly for call hold
|
||||
[\#1728](https://github.com/matrix-org/matrix-js-sdk/pull/1728)
|
||||
* Stop breeding sync listeners
|
||||
[\#1727](https://github.com/matrix-org/matrix-js-sdk/pull/1727)
|
||||
* Fix semicolons in TS files
|
||||
[\#1724](https://github.com/matrix-org/matrix-js-sdk/pull/1724)
|
||||
* [BREAKING] Convert MatrixClient to TypeScript
|
||||
[\#1718](https://github.com/matrix-org/matrix-js-sdk/pull/1718)
|
||||
* Factor out backup management to a separate module
|
||||
[\#1697](https://github.com/matrix-org/matrix-js-sdk/pull/1697)
|
||||
* Ignore power_levels events with unknown state_key on room-state
|
||||
initialization
|
||||
[\#1723](https://github.com/matrix-org/matrix-js-sdk/pull/1723)
|
||||
* Revert 1579 (Fix extra negotiate message in Firefox)
|
||||
[\#1725](https://github.com/matrix-org/matrix-js-sdk/pull/1725)
|
||||
|
||||
Changes in [11.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.2.0) (2021-06-07)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.2.0-rc.1...v11.2.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [11.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.2.0-rc.1) (2021-06-01)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.1.0...v11.2.0-rc.1)
|
||||
|
||||
* Switch to stable endpoint/fields for MSC2858
|
||||
[\#1720](https://github.com/matrix-org/matrix-js-sdk/pull/1720)
|
||||
* Bump ws from 7.4.2 to 7.4.6
|
||||
[\#1715](https://github.com/matrix-org/matrix-js-sdk/pull/1715)
|
||||
* Make consistent call event type checks
|
||||
[\#1712](https://github.com/matrix-org/matrix-js-sdk/pull/1712)
|
||||
* Apply new Babel linting config
|
||||
[\#1714](https://github.com/matrix-org/matrix-js-sdk/pull/1714)
|
||||
* Bump browserslist from 4.16.1 to 4.16.6
|
||||
[\#1709](https://github.com/matrix-org/matrix-js-sdk/pull/1709)
|
||||
* Add user_busy call hangup reason
|
||||
[\#1713](https://github.com/matrix-org/matrix-js-sdk/pull/1713)
|
||||
* 👕 New linting rules
|
||||
[\#1688](https://github.com/matrix-org/matrix-js-sdk/pull/1688)
|
||||
* Emit relations created when target event added later
|
||||
[\#1710](https://github.com/matrix-org/matrix-js-sdk/pull/1710)
|
||||
* Bump libolm version and update package name.
|
||||
[\#1705](https://github.com/matrix-org/matrix-js-sdk/pull/1705)
|
||||
* Fix uploadContent not rejecting promise when http status code >= 400
|
||||
[\#1703](https://github.com/matrix-org/matrix-js-sdk/pull/1703)
|
||||
* Reduce noise in tests
|
||||
[\#1702](https://github.com/matrix-org/matrix-js-sdk/pull/1702)
|
||||
* Only log once if a Room lacks an m.room.create event
|
||||
[\#1700](https://github.com/matrix-org/matrix-js-sdk/pull/1700)
|
||||
* Cache normalized room name
|
||||
[\#1701](https://github.com/matrix-org/matrix-js-sdk/pull/1701)
|
||||
* Change call event handlers to adapt to undecrypted events
|
||||
[\#1698](https://github.com/matrix-org/matrix-js-sdk/pull/1698)
|
||||
|
||||
Changes in [11.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.1.0) (2021-05-24)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.1.0-rc.1...v11.1.0)
|
||||
|
||||
* [Release] Bump libolm version and update package name
|
||||
[\#1707](https://github.com/matrix-org/matrix-js-sdk/pull/1707)
|
||||
* [Release] Change call event handlers to adapt to undecrypted events
|
||||
[\#1699](https://github.com/matrix-org/matrix-js-sdk/pull/1699)
|
||||
|
||||
Changes in [11.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.1.0-rc.1) (2021-05-19)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.0.0...v11.1.0-rc.1)
|
||||
|
||||
* Fix regressed glare
|
||||
[\#1690](https://github.com/matrix-org/matrix-js-sdk/pull/1690)
|
||||
* Add m.reaction to EventType enum
|
||||
[\#1692](https://github.com/matrix-org/matrix-js-sdk/pull/1692)
|
||||
* Prioritise and reduce the amount of events decrypted on application startup
|
||||
[\#1684](https://github.com/matrix-org/matrix-js-sdk/pull/1684)
|
||||
* Decrypt relations before applying them to target event
|
||||
[\#1696](https://github.com/matrix-org/matrix-js-sdk/pull/1696)
|
||||
* Guard against duplicates in `Relations` model
|
||||
|
||||
Changes in [11.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.0.0) (2021-05-17)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.0.0-rc.1...v11.0.0)
|
||||
|
||||
* [Release] Fix regressed glare
|
||||
[\#1695](https://github.com/matrix-org/matrix-js-sdk/pull/1695)
|
||||
|
||||
Changes in [11.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.0.0-rc.1) (2021-05-11)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.1.0...v11.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `MatrixCall` and related APIs have been redesigned to support multiple streams
|
||||
(see [\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660) for more details)
|
||||
|
||||
All changes
|
||||
---
|
||||
|
||||
* Switch from MSC1772 unstable prefixes to stable
|
||||
[\#1679](https://github.com/matrix-org/matrix-js-sdk/pull/1679)
|
||||
* Update the VoIP example to work with the new changes
|
||||
[\#1680](https://github.com/matrix-org/matrix-js-sdk/pull/1680)
|
||||
* Bump hosted-git-info from 2.8.8 to 2.8.9
|
||||
[\#1687](https://github.com/matrix-org/matrix-js-sdk/pull/1687)
|
||||
* Support for multiple streams (not MSC3077)
|
||||
[\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660)
|
||||
* Tweak missing m.room.create errors to describe their source
|
||||
[\#1683](https://github.com/matrix-org/matrix-js-sdk/pull/1683)
|
||||
|
||||
Changes in [10.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.1.0) (2021-05-10)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.1.0-rc.1...v10.1.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [10.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.1.0-rc.1) (2021-05-04)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.0.0...v10.1.0-rc.1)
|
||||
|
||||
* Revert "Raise logging dramatically to chase pending event errors"
|
||||
[\#1681](https://github.com/matrix-org/matrix-js-sdk/pull/1681)
|
||||
* Add test coverage collection script
|
||||
[\#1677](https://github.com/matrix-org/matrix-js-sdk/pull/1677)
|
||||
* Raise logging dramatically to chase pending event errors
|
||||
[\#1678](https://github.com/matrix-org/matrix-js-sdk/pull/1678)
|
||||
* Support MSC3086 asserted identity
|
||||
[\#1674](https://github.com/matrix-org/matrix-js-sdk/pull/1674)
|
||||
* Fix `/search` with no results field work again
|
||||
[\#1670](https://github.com/matrix-org/matrix-js-sdk/pull/1670)
|
||||
* Add room.getMembers method
|
||||
[\#1672](https://github.com/matrix-org/matrix-js-sdk/pull/1672)
|
||||
|
||||
Changes in [10.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.0.0) (2021-04-26)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.0.0-rc.1...v10.0.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [10.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.0.0-rc.1) (2021-04-21)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.11.0...v10.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* The `RoomState.members` event is now only emitted when the room member's power level or the room's normal power level actually changes
|
||||
|
||||
All changes
|
||||
---
|
||||
|
||||
* Restrict event emit for room members that had power levels changed
|
||||
[\#1675](https://github.com/matrix-org/matrix-js-sdk/pull/1675)
|
||||
* Fix sync with misconfigured push rules
|
||||
[\#1669](https://github.com/matrix-org/matrix-js-sdk/pull/1669)
|
||||
* Add missing await
|
||||
[\#1665](https://github.com/matrix-org/matrix-js-sdk/pull/1665)
|
||||
* Migrate to `eslint-plugin-matrix-org`
|
||||
[\#1642](https://github.com/matrix-org/matrix-js-sdk/pull/1642)
|
||||
* Add missing event type enum for key verification done
|
||||
[\#1664](https://github.com/matrix-org/matrix-js-sdk/pull/1664)
|
||||
* Fix timeline jumpiness by setting correct txnId
|
||||
[\#1663](https://github.com/matrix-org/matrix-js-sdk/pull/1663)
|
||||
* Fix calling addEventListener if it does not exist
|
||||
[\#1661](https://github.com/matrix-org/matrix-js-sdk/pull/1661)
|
||||
* Persist unsent messages for subsequent sessions
|
||||
[\#1655](https://github.com/matrix-org/matrix-js-sdk/pull/1655)
|
||||
|
||||
Changes in [9.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.11.0) (2021-04-12)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.11.0-rc.1...v9.11.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.11.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.11.0-rc.1) (2021-04-07)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.10.0...v9.11.0-rc.1)
|
||||
|
||||
* Only try to cache private keys we know exist
|
||||
[\#1657](https://github.com/matrix-org/matrix-js-sdk/pull/1657)
|
||||
* Properly terminate screen-share calls if NoUserMedia
|
||||
[\#1654](https://github.com/matrix-org/matrix-js-sdk/pull/1654)
|
||||
* Attended transfer
|
||||
[\#1652](https://github.com/matrix-org/matrix-js-sdk/pull/1652)
|
||||
* Remove catch handlers in private key retrieval
|
||||
[\#1653](https://github.com/matrix-org/matrix-js-sdk/pull/1653)
|
||||
* Fixed the media fail error on caller's side
|
||||
[\#1651](https://github.com/matrix-org/matrix-js-sdk/pull/1651)
|
||||
* Add function to share megolm keys for historical messages, take 2
|
||||
[\#1640](https://github.com/matrix-org/matrix-js-sdk/pull/1640)
|
||||
* Cache cross-signing private keys if needed on bootstrap
|
||||
[\#1649](https://github.com/matrix-org/matrix-js-sdk/pull/1649)
|
||||
|
||||
Changes in [9.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.10.0) (2021-03-29)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.10.0-rc.1...v9.10.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.10.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.10.0-rc.1) (2021-03-25)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.9.0...v9.10.0-rc.1)
|
||||
|
||||
* Don't send m.call.hangup if m.call.invite wasn't sent either
|
||||
[\#1647](https://github.com/matrix-org/matrix-js-sdk/pull/1647)
|
||||
* docs: registerGuest()
|
||||
[\#1641](https://github.com/matrix-org/matrix-js-sdk/pull/1641)
|
||||
* Download device keys in chunks of 250
|
||||
[\#1639](https://github.com/matrix-org/matrix-js-sdk/pull/1639)
|
||||
* More VoIP connectivity fixes
|
||||
[\#1646](https://github.com/matrix-org/matrix-js-sdk/pull/1646)
|
||||
* Make selectDesktopCapturerSource param optional
|
||||
[\#1644](https://github.com/matrix-org/matrix-js-sdk/pull/1644)
|
||||
* Expose APIs needed for reworked cross-signing login flow
|
||||
[\#1632](https://github.com/matrix-org/matrix-js-sdk/pull/1632)
|
||||
|
||||
Changes in [9.9.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.9.0) (2021-03-15)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.9.0-rc.1...v9.9.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.9.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.9.0-rc.1) (2021-03-10)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.8.0...v9.9.0-rc.1)
|
||||
|
||||
* Remove detailed Olm session logging
|
||||
[\#1638](https://github.com/matrix-org/matrix-js-sdk/pull/1638)
|
||||
* Add space summary suggested only param
|
||||
[\#1637](https://github.com/matrix-org/matrix-js-sdk/pull/1637)
|
||||
* Check TURN servers periodically, and at start of calls
|
||||
[\#1634](https://github.com/matrix-org/matrix-js-sdk/pull/1634)
|
||||
* Support sending invite reasons
|
||||
[\#1624](https://github.com/matrix-org/matrix-js-sdk/pull/1624)
|
||||
* Bump elliptic from 6.5.3 to 6.5.4
|
||||
[\#1636](https://github.com/matrix-org/matrix-js-sdk/pull/1636)
|
||||
* Add a function to get a room's MXC URI
|
||||
[\#1635](https://github.com/matrix-org/matrix-js-sdk/pull/1635)
|
||||
* Stop streams if the call has ended
|
||||
[\#1633](https://github.com/matrix-org/matrix-js-sdk/pull/1633)
|
||||
* Remove export keyword from global.d.ts
|
||||
[\#1631](https://github.com/matrix-org/matrix-js-sdk/pull/1631)
|
||||
* Fix IndexedDB store creation example
|
||||
[\#1445](https://github.com/matrix-org/matrix-js-sdk/pull/1445)
|
||||
* An attempt to cleanup how constraints are handled in calls
|
||||
[\#1613](https://github.com/matrix-org/matrix-js-sdk/pull/1613)
|
||||
* Extract display name patterns to constants
|
||||
[\#1628](https://github.com/matrix-org/matrix-js-sdk/pull/1628)
|
||||
* Bump pug-code-gen from 2.0.2 to 2.0.3
|
||||
[\#1630](https://github.com/matrix-org/matrix-js-sdk/pull/1630)
|
||||
* Avoid deadlocks when ensuring Olm sessions for devices
|
||||
[\#1627](https://github.com/matrix-org/matrix-js-sdk/pull/1627)
|
||||
* Filter out edits from other senders in history
|
||||
[\#1626](https://github.com/matrix-org/matrix-js-sdk/pull/1626)
|
||||
* Fix ContentHelpers export
|
||||
[\#1618](https://github.com/matrix-org/matrix-js-sdk/pull/1618)
|
||||
* Add logging to in progress Olm sessions
|
||||
[\#1621](https://github.com/matrix-org/matrix-js-sdk/pull/1621)
|
||||
* Don't ignore ICE candidates received before offer/answer
|
||||
[\#1623](https://github.com/matrix-org/matrix-js-sdk/pull/1623)
|
||||
* Better handling of send failures on VoIP events
|
||||
[\#1622](https://github.com/matrix-org/matrix-js-sdk/pull/1622)
|
||||
* Log when turn creds expire
|
||||
[\#1620](https://github.com/matrix-org/matrix-js-sdk/pull/1620)
|
||||
* Initial Spaces [MSC1772] support
|
||||
[\#1563](https://github.com/matrix-org/matrix-js-sdk/pull/1563)
|
||||
* Add logging to crypto store transactions
|
||||
[\#1617](https://github.com/matrix-org/matrix-js-sdk/pull/1617)
|
||||
* Room helper for getting type and checking if it is a space room
|
||||
[\#1610](https://github.com/matrix-org/matrix-js-sdk/pull/1610)
|
||||
|
||||
Changes in [9.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.8.0) (2021-03-01)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.8.0-rc.1...v9.8.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.8.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.8.0-rc.1) (2021-02-24)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.7.0...v9.8.0-rc.1)
|
||||
|
||||
* Optimise prefixed logger
|
||||
[\#1615](https://github.com/matrix-org/matrix-js-sdk/pull/1615)
|
||||
* Add debug logs to encryption prep, take 3
|
||||
[\#1614](https://github.com/matrix-org/matrix-js-sdk/pull/1614)
|
||||
* Add functions for upper & lowercase random strings
|
||||
[\#1612](https://github.com/matrix-org/matrix-js-sdk/pull/1612)
|
||||
* Room helpers for invite permissions and join rules
|
||||
[\#1609](https://github.com/matrix-org/matrix-js-sdk/pull/1609)
|
||||
* Fixed wording in "Adding video track with id" log
|
||||
[\#1606](https://github.com/matrix-org/matrix-js-sdk/pull/1606)
|
||||
* Add more debug logs to encryption prep
|
||||
[\#1605](https://github.com/matrix-org/matrix-js-sdk/pull/1605)
|
||||
* Add option to set ice candidate pool size
|
||||
[\#1604](https://github.com/matrix-org/matrix-js-sdk/pull/1604)
|
||||
* Cancel call if no source was selected
|
||||
[\#1601](https://github.com/matrix-org/matrix-js-sdk/pull/1601)
|
||||
|
||||
Changes in [9.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.7.0) (2021-02-16)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.7.0-rc.1...v9.7.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.7.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.7.0-rc.1) (2021-02-10)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.6.0...v9.7.0-rc.1)
|
||||
|
||||
* Handle undefined peerconn
|
||||
[\#1600](https://github.com/matrix-org/matrix-js-sdk/pull/1600)
|
||||
* ReEmitter: Don't throw if no error handler is attached
|
||||
[\#1599](https://github.com/matrix-org/matrix-js-sdk/pull/1599)
|
||||
* Convert ReEmitter to TS
|
||||
[\#1598](https://github.com/matrix-org/matrix-js-sdk/pull/1598)
|
||||
* Fix typo in main readme
|
||||
[\#1597](https://github.com/matrix-org/matrix-js-sdk/pull/1597)
|
||||
* Remove rogue plus character
|
||||
[\#1596](https://github.com/matrix-org/matrix-js-sdk/pull/1596)
|
||||
* Fix call ID NaN
|
||||
[\#1595](https://github.com/matrix-org/matrix-js-sdk/pull/1595)
|
||||
* Fix Electron type merging
|
||||
[\#1594](https://github.com/matrix-org/matrix-js-sdk/pull/1594)
|
||||
* Fix browser screen share
|
||||
[\#1593](https://github.com/matrix-org/matrix-js-sdk/pull/1593)
|
||||
* Fix desktop Matrix screen sharing
|
||||
[\#1570](https://github.com/matrix-org/matrix-js-sdk/pull/1570)
|
||||
* Guard against confused server retry times
|
||||
[\#1591](https://github.com/matrix-org/matrix-js-sdk/pull/1591)
|
||||
* Decrypt redaction events
|
||||
[\#1589](https://github.com/matrix-org/matrix-js-sdk/pull/1589)
|
||||
* Fix edge cases with peeking where a room is re-peeked
|
||||
[\#1587](https://github.com/matrix-org/matrix-js-sdk/pull/1587)
|
||||
|
||||
Changes in [9.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.6.0) (2021-02-03)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.6.0-rc.1...v9.6.0)
|
||||
|
||||
* [Release] Fix edge cases with peeking where a room is re-peeked
|
||||
[\#1588](https://github.com/matrix-org/matrix-js-sdk/pull/1588)
|
||||
|
||||
Changes in [9.6.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.6.0-rc.1) (2021-01-29)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.5.1...v9.6.0-rc.1)
|
||||
|
||||
* Add support for getting call stats
|
||||
[\#1584](https://github.com/matrix-org/matrix-js-sdk/pull/1584)
|
||||
* Fix compatibility with v0 calls
|
||||
[\#1583](https://github.com/matrix-org/matrix-js-sdk/pull/1583)
|
||||
* Upgrade deps 2021-01
|
||||
[\#1582](https://github.com/matrix-org/matrix-js-sdk/pull/1582)
|
||||
* Log the call ID when logging that we've received VoIP events
|
||||
[\#1581](https://github.com/matrix-org/matrix-js-sdk/pull/1581)
|
||||
* Fix extra negotiate message in Firefox
|
||||
[\#1579](https://github.com/matrix-org/matrix-js-sdk/pull/1579)
|
||||
* Add debug logs to encryption prep
|
||||
[\#1580](https://github.com/matrix-org/matrix-js-sdk/pull/1580)
|
||||
* Expose getPresence endpoint
|
||||
[\#1578](https://github.com/matrix-org/matrix-js-sdk/pull/1578)
|
||||
* Queue keys for backup even if backup isn't enabled yet
|
||||
[\#1577](https://github.com/matrix-org/matrix-js-sdk/pull/1577)
|
||||
* Stop retrying TURN access when forbidden
|
||||
[\#1576](https://github.com/matrix-org/matrix-js-sdk/pull/1576)
|
||||
* Add DTMF sending support
|
||||
[\#1573](https://github.com/matrix-org/matrix-js-sdk/pull/1573)
|
||||
|
||||
Changes in [9.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.5.1) (2021-01-26)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.5.0...v9.5.1)
|
||||
|
||||
* [Release] Fix compatibility with v0 calls
|
||||
[\#1585](https://github.com/matrix-org/matrix-js-sdk/pull/1585)
|
||||
|
||||
Changes in [9.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.5.0) (2021-01-18)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.5.0-rc.1...v9.5.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.5.0-rc.1) (2021-01-13)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.1...v9.5.0-rc.1)
|
||||
|
||||
* Don't log if no WebRTC
|
||||
[\#1574](https://github.com/matrix-org/matrix-js-sdk/pull/1574)
|
||||
* Add _unstable_getSharedRooms
|
||||
[\#1417](https://github.com/matrix-org/matrix-js-sdk/pull/1417)
|
||||
* Bump node-notifier from 8.0.0 to 8.0.1
|
||||
[\#1568](https://github.com/matrix-org/matrix-js-sdk/pull/1568)
|
||||
* Ignore party ID if opponent is v0
|
||||
[\#1567](https://github.com/matrix-org/matrix-js-sdk/pull/1567)
|
||||
* Basic call transfer initiation support
|
||||
[\#1566](https://github.com/matrix-org/matrix-js-sdk/pull/1566)
|
||||
* Room version 6 is now a thing
|
||||
[\#1572](https://github.com/matrix-org/matrix-js-sdk/pull/1572)
|
||||
* Store keys with same index but better trust level
|
||||
[\#1571](https://github.com/matrix-org/matrix-js-sdk/pull/1571)
|
||||
* Use TypeScript source for development, swap to build during release
|
||||
[\#1561](https://github.com/matrix-org/matrix-js-sdk/pull/1561)
|
||||
* Revert "Ignore party ID if opponent is v0"
|
||||
[\#1565](https://github.com/matrix-org/matrix-js-sdk/pull/1565)
|
||||
* Basic call transfer initiation support
|
||||
[\#1558](https://github.com/matrix-org/matrix-js-sdk/pull/1558)
|
||||
* Ignore party ID if opponent is v0
|
||||
[\#1559](https://github.com/matrix-org/matrix-js-sdk/pull/1559)
|
||||
* Honour a call reject event from another of our own devices
|
||||
[\#1562](https://github.com/matrix-org/matrix-js-sdk/pull/1562)
|
||||
|
||||
Changes in [9.4.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.1) (2020-12-21)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.0...v9.4.1)
|
||||
|
||||
* Further script tweaks to get all layers building again
|
||||
|
||||
Changes in [9.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.0) (2020-12-21)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.0-rc.2...v9.4.0)
|
||||
|
||||
* Revert `postinstall` script change, causes issues for other layers
|
||||
|
||||
Changes in [9.4.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.0-rc.2) (2020-12-16)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.0-rc.1...v9.4.0-rc.2)
|
||||
|
||||
* Remove `postinstall` script which also runs as a dependency
|
||||
[\#1560](https://github.com/matrix-org/matrix-js-sdk/pull/1560)
|
||||
|
||||
Changes in [9.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.0-rc.1) (2020-12-16)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.3.0...v9.4.0-rc.1)
|
||||
|
||||
* Fixes to support line 1 / 2
|
||||
[\#1553](https://github.com/matrix-org/matrix-js-sdk/pull/1553)
|
||||
* Add API for listening to remote hold status, advertise VoIP V1
|
||||
[\#1549](https://github.com/matrix-org/matrix-js-sdk/pull/1549)
|
||||
* A hangup from another client is still valid
|
||||
[\#1555](https://github.com/matrix-org/matrix-js-sdk/pull/1555)
|
||||
* Remove temporary build step for tests
|
||||
[\#1554](https://github.com/matrix-org/matrix-js-sdk/pull/1554)
|
||||
* Move browser build steps to prepublish only
|
||||
[\#1552](https://github.com/matrix-org/matrix-js-sdk/pull/1552)
|
||||
* Extend getSsoLoginUrl for MSC2858
|
||||
[\#1541](https://github.com/matrix-org/matrix-js-sdk/pull/1541)
|
||||
|
||||
Changes in [9.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.3.0) (2020-12-07)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.3.0-rc.1...v9.3.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.3.0-rc.1) (2020-12-02)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.2.0...v9.3.0-rc.1)
|
||||
|
||||
* Export CallError
|
||||
[\#1551](https://github.com/matrix-org/matrix-js-sdk/pull/1551)
|
||||
* Upgrade dependencies
|
||||
[\#1550](https://github.com/matrix-org/matrix-js-sdk/pull/1550)
|
||||
* Don't log error when environment does not support WebRTC
|
||||
[\#1547](https://github.com/matrix-org/matrix-js-sdk/pull/1547)
|
||||
* Fix dehydration method name
|
||||
[\#1544](https://github.com/matrix-org/matrix-js-sdk/pull/1544)
|
||||
|
||||
Changes in [9.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.2.0) (2020-11-23)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.2.0-rc.1...v9.2.0)
|
||||
|
||||
* [Release] Fix dehydration method name
|
||||
[\#1545](https://github.com/matrix-org/matrix-js-sdk/pull/1545)
|
||||
|
||||
Changes in [9.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.2.0-rc.1) (2020-11-18)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.1.0...v9.2.0-rc.1)
|
||||
|
||||
* Implement call holding functionality
|
||||
[\#1532](https://github.com/matrix-org/matrix-js-sdk/pull/1532)
|
||||
* Support awaitable one-time dehydration
|
||||
[\#1537](https://github.com/matrix-org/matrix-js-sdk/pull/1537)
|
||||
* Client set profile methods update own user
|
||||
[\#1534](https://github.com/matrix-org/matrix-js-sdk/pull/1534)
|
||||
|
||||
Changes in [9.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.1.0) (2020-11-09)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.1.0-rc.1...v9.1.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.1.0-rc.1) (2020-11-04)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.0.1...v9.1.0-rc.1)
|
||||
|
||||
* Fix spelling error in the server ACL event type
|
||||
[\#1535](https://github.com/matrix-org/matrix-js-sdk/pull/1535)
|
||||
* await idb operations from crypto store for dehydration
|
||||
[\#1533](https://github.com/matrix-org/matrix-js-sdk/pull/1533)
|
||||
* Fix stuck never-sending messages
|
||||
[\#1531](https://github.com/matrix-org/matrix-js-sdk/pull/1531)
|
||||
* Await key cache check to avoid prompts
|
||||
[\#1529](https://github.com/matrix-org/matrix-js-sdk/pull/1529)
|
||||
* Improve ICE candidate batching
|
||||
[\#1524](https://github.com/matrix-org/matrix-js-sdk/pull/1524)
|
||||
* Convert logger to typescript
|
||||
[\#1527](https://github.com/matrix-org/matrix-js-sdk/pull/1527)
|
||||
* Fix logger typo
|
||||
[\#1525](https://github.com/matrix-org/matrix-js-sdk/pull/1525)
|
||||
* bind online listener to window instead of document
|
||||
[\#1523](https://github.com/matrix-org/matrix-js-sdk/pull/1523)
|
||||
* Support m.call.select_answer
|
||||
[\#1522](https://github.com/matrix-org/matrix-js-sdk/pull/1522)
|
||||
|
||||
Changes in [9.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.0.1) (2020-10-28)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.0.0...v9.0.1)
|
||||
|
||||
* [Release] Await key cache check to avoid prompts
|
||||
[\#1530](https://github.com/matrix-org/matrix-js-sdk/pull/1530)
|
||||
|
||||
Changes in [9.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.0.0) (2020-10-26)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.0.0-rc.1...v9.0.0)
|
||||
|
||||
* Fix logger typo
|
||||
[\#1528](https://github.com/matrix-org/matrix-js-sdk/pull/1528)
|
||||
|
||||
Changes in [9.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.0.0-rc.1) (2020-10-21)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.5.0...v9.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `hasPendingEvent` now returns false instead of throwing when pending ordering mode is not `detached`
|
||||
|
||||
All changes
|
||||
---
|
||||
|
||||
* Don't cache failures when fetching /versions
|
||||
[\#1521](https://github.com/matrix-org/matrix-js-sdk/pull/1521)
|
||||
* Install deps first as part of release
|
||||
[\#1518](https://github.com/matrix-org/matrix-js-sdk/pull/1518)
|
||||
* [Breaking] Change hasPendingEvent to return false if pending ordering
|
||||
!detached
|
||||
[\#1517](https://github.com/matrix-org/matrix-js-sdk/pull/1517)
|
||||
* Skip editor prompts for merges
|
||||
[\#1519](https://github.com/matrix-org/matrix-js-sdk/pull/1519)
|
||||
* Convert call test to TypeScript
|
||||
[\#1516](https://github.com/matrix-org/matrix-js-sdk/pull/1516)
|
||||
* Support party_id
|
||||
[\#1512](https://github.com/matrix-org/matrix-js-sdk/pull/1512)
|
||||
* Support m.call.reject
|
||||
[\#1510](https://github.com/matrix-org/matrix-js-sdk/pull/1510)
|
||||
* Remove specbuild from .gitignore
|
||||
[\#1515](https://github.com/matrix-org/matrix-js-sdk/pull/1515)
|
||||
* Log the error when we failed to send candidates
|
||||
[\#1514](https://github.com/matrix-org/matrix-js-sdk/pull/1514)
|
||||
* Fixes for call state machine
|
||||
[\#1503](https://github.com/matrix-org/matrix-js-sdk/pull/1503)
|
||||
* Fix call event handler listener removing
|
||||
[\#1506](https://github.com/matrix-org/matrix-js-sdk/pull/1506)
|
||||
* Set the type of the call based on the tracks
|
||||
[\#1501](https://github.com/matrix-org/matrix-js-sdk/pull/1501)
|
||||
* Use new local timestamp for calls
|
||||
[\#1499](https://github.com/matrix-org/matrix-js-sdk/pull/1499)
|
||||
* Adjust types and APIs to match React SDK
|
||||
[\#1502](https://github.com/matrix-org/matrix-js-sdk/pull/1502)
|
||||
* Make an accurate version of 'age' for events
|
||||
[\#1495](https://github.com/matrix-org/matrix-js-sdk/pull/1495)
|
||||
* Make 'options' parameter optional
|
||||
[\#1498](https://github.com/matrix-org/matrix-js-sdk/pull/1498)
|
||||
* Create a giant event type enum
|
||||
[\#1497](https://github.com/matrix-org/matrix-js-sdk/pull/1497)
|
||||
* Convert call.js to Typescript & update WebRTC APIs (re-apply)
|
||||
[\#1494](https://github.com/matrix-org/matrix-js-sdk/pull/1494)
|
||||
|
||||
Changes in [8.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.5.0) (2020-10-12)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.5.0-rc.1...v8.5.0)
|
||||
|
||||
@@ -307,7 +307,7 @@ 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 necessry to call ``matrixClient.initCrypto()`` after creating a new
|
||||
It is also necessary to call ``matrixClient.initCrypto()`` after creating a new
|
||||
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
|
||||
initialise the crypto layer.
|
||||
|
||||
|
||||
@@ -31,6 +31,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() {
|
||||
@@ -62,10 +79,7 @@ function syncComplete() {
|
||||
);
|
||||
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);
|
||||
};
|
||||
|
||||
+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>
|
||||
+50
-33
@@ -1,23 +1,25 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "8.5.0",
|
||||
"version": "12.0.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"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 clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:compile-browser && yarn build:minify-browser && yarn build:types",
|
||||
"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 --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.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 76 src spec",
|
||||
"lint:js": "eslint --max-warnings 57 src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest spec/ --coverage --testEnvironment node",
|
||||
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -27,10 +29,11 @@
|
||||
"matrix-org"
|
||||
],
|
||||
"main": "./lib/index.js",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"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",
|
||||
"files": [
|
||||
@@ -46,51 +49,65 @@
|
||||
"release.sh"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"another-json": "^0.2.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.0",
|
||||
"qs": "^6.9.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"qs": "^6.9.6",
|
||||
"request": "^2.88.2",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.11.6",
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.10.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
|
||||
"@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.11.5",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@babel/register": "^7.11.5",
|
||||
"@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.3.tgz",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "12",
|
||||
"@types/request": "^2.48.5",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^24.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
||||
"@typescript-eslint/parser": "^4.17.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.3.2",
|
||||
"browserify": "^16.5.2",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "7.9.0",
|
||||
"eslint-config-matrix-org": "^0.1.2",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint": "7.18.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"jest": "^24.9.0",
|
||||
"jest-localstorage-mock": "^2.4.3",
|
||||
"jest": "^26.6.3",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jsdoc": "^3.6.6",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^4.8.0",
|
||||
"tsify": "^4.0.2",
|
||||
"typescript": "^3.9.7"
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
"testEnvironment": "node",
|
||||
"testMatch": [
|
||||
"<rootDir>/spec/**/*.spec.{js,ts}"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
+48
-7
@@ -94,6 +94,14 @@ 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
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
# update_changelog doesn't have a --version flag
|
||||
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
|
||||
@@ -170,6 +178,19 @@ echo "yarn version"
|
||||
# manually commit the result.
|
||||
yarn version --no-git-tag-version --new-version "$release"
|
||||
|
||||
# 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
|
||||
|
||||
# commit yarn.lock if it exists, is versioned, and is modified
|
||||
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
|
||||
then
|
||||
@@ -204,11 +225,6 @@ if [ $dodist -eq 0 ]; then
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
git checkout "$rel_branch"
|
||||
# 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
|
||||
yarn install
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
@@ -340,7 +356,7 @@ fi
|
||||
echo "updating master branch"
|
||||
git checkout master
|
||||
git pull
|
||||
git merge "$rel_branch"
|
||||
git merge "$rel_branch" --no-edit
|
||||
|
||||
# push master to github
|
||||
git push origin master
|
||||
@@ -349,6 +365,31 @@ git push origin master
|
||||
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
|
||||
git checkout develop
|
||||
git pull
|
||||
git merge master
|
||||
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
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
|
||||
export class MockBlob {
|
||||
private contents: number[] = [];
|
||||
|
||||
public constructor(private parts: ArrayLike<number>[]) {
|
||||
parts.forEach(p => Array.from(p).forEach(e => this.contents.push(e)));
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.contents.length;
|
||||
}
|
||||
}
|
||||
+18
-14
@@ -20,12 +20,12 @@ limitations under the License.
|
||||
import './olm-loader';
|
||||
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
import {LocalStorageCryptoStore} from '../src/crypto/store/localStorage-crypto-store';
|
||||
import {logger} from '../src/logger';
|
||||
import {WebStorageSessionStore} from "../src/store/session/webstorage";
|
||||
import {syncPromise} from "./test-utils";
|
||||
import {createClient} from "../src/matrix";
|
||||
import {MockStorageApi} from "./MockStorageApi";
|
||||
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
|
||||
import { logger } from '../src/logger';
|
||||
import { WebStorageSessionStore } from "../src/store/session/webstorage";
|
||||
import { syncPromise } from "./test-utils";
|
||||
import { createClient } from "../src/matrix";
|
||||
import { MockStorageApi } from "./MockStorageApi";
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
@@ -69,6 +69,9 @@ export function TestClient(
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = {};
|
||||
this.callEventHandler = {
|
||||
calls: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
TestClient.prototype.toString = function() {
|
||||
@@ -126,11 +129,10 @@ TestClient.prototype.expectDeviceKeyUpload = function() {
|
||||
expect(Object.keys(self.oneTimeKeys).length).toEqual(0);
|
||||
|
||||
self.deviceKeys = content.device_keys;
|
||||
return {one_time_key_counts: {signed_curve25519: 0}};
|
||||
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
|
||||
@@ -148,9 +150,9 @@ TestClient.prototype.awaitOneTimeKeyUpload = function() {
|
||||
.respond(200, (path, content) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
return {one_time_key_counts: {
|
||||
return { one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
}};
|
||||
} };
|
||||
});
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
@@ -161,9 +163,9 @@ TestClient.prototype.awaitOneTimeKeyUpload = function() {
|
||||
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: {
|
||||
return { one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
}};
|
||||
} };
|
||||
});
|
||||
|
||||
// this can take ages
|
||||
@@ -194,7 +196,6 @@ TestClient.prototype.expectKeyQuery = function(response) {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
@@ -205,7 +206,6 @@ TestClient.prototype.getDeviceKey = function() {
|
||||
return this.deviceKeys.keys[keyId];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
@@ -230,3 +230,7 @@ TestClient.prototype.flushSync = function() {
|
||||
logger.log(`${this}: flushSync completed`);
|
||||
});
|
||||
};
|
||||
|
||||
TestClient.prototype.isFallbackICEServerAllowed = function() {
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -17,10 +17,10 @@ limitations under the License.
|
||||
// load XmlHttpRequest mock
|
||||
import "./setupTests";
|
||||
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
import {MockStorageApi} from "../MockStorageApi";
|
||||
import {WebStorageSessionStore} from "../../src/store/session/webstorage";
|
||||
import { MockStorageApi } from "../MockStorageApi";
|
||||
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
import {LocalStorageCryptoStore} from "../../src/crypto/store/localStorage-crypto-store";
|
||||
import { LocalStorageCryptoStore } from "../../src/crypto/store/localStorage-crypto-store";
|
||||
import * as utils from "../test-utils";
|
||||
|
||||
const USER_ID = "@user:test.server";
|
||||
@@ -58,7 +58,7 @@ describe("Browserify Test", function() {
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
({client, httpBackend} = await createTestClient());
|
||||
({ client, httpBackend } = await createTestClient());
|
||||
await client.startClient();
|
||||
});
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {TestClient} from '../TestClient';
|
||||
import { TestClient } from '../TestClient';
|
||||
import * as testUtils from '../test-utils';
|
||||
import {logger} from '../../src/logger';
|
||||
import { logger } from '../../src/logger';
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -67,7 +67,6 @@ function getSyncResponse(roomMembers) {
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
|
||||
describe("DeviceList management:", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('not running deviceList tests: Olm not present');
|
||||
@@ -98,7 +97,7 @@ describe("DeviceList management:", function() {
|
||||
});
|
||||
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(function() {
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
@@ -137,11 +136,10 @@ describe("DeviceList management:", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("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': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
|
||||
@@ -160,14 +158,14 @@ describe("DeviceList management:", function() {
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
|
||||
200, {event_id: '$event1'});
|
||||
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(),
|
||||
aliceTestClient.client.crypto._deviceList.saveIfDirty(),
|
||||
]);
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
@@ -199,12 +197,12 @@ describe("DeviceList management:", function() {
|
||||
},
|
||||
token: '3',
|
||||
}).respond(200, {
|
||||
device_keys: {'@chris:abc': {}},
|
||||
device_keys: { '@chris:abc': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
return aliceTestClient.client.crypto._deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
@@ -228,7 +226,7 @@ describe("DeviceList management:", function() {
|
||||
},
|
||||
token: '2',
|
||||
}).respond(200, {
|
||||
device_keys: {'@bob:xyz': {}},
|
||||
device_keys: { '@bob:xyz': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
@@ -237,7 +235,7 @@ describe("DeviceList management:", function() {
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
return aliceTestClient.client.crypto._deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
@@ -258,7 +256,7 @@ describe("DeviceList management:", function() {
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
return aliceTestClient.client.crypto._deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
@@ -288,7 +286,7 @@ describe("DeviceList management:", function() {
|
||||
},
|
||||
);
|
||||
await aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
await aliceTestClient.client.crypto._deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
@@ -323,9 +321,8 @@ describe("DeviceList management:", function() {
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
await aliceTestClient.client.crypto._deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
@@ -361,7 +358,7 @@ describe("DeviceList management:", function() {
|
||||
);
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
await aliceTestClient.client.crypto._deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
@@ -382,7 +379,7 @@ describe("DeviceList management:", function() {
|
||||
anotherTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse([]));
|
||||
await anotherTestClient.flushSync();
|
||||
await anotherTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
await anotherTestClient.client.crypto._deviceList.saveIfDirty();
|
||||
|
||||
anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
@@ -28,11 +28,10 @@ limitations under the License.
|
||||
// load olm before the sdk if possible
|
||||
import '../olm-loader';
|
||||
|
||||
import {logger} from '../../src/logger';
|
||||
import { logger } from '../../src/logger';
|
||||
import * as testUtils from "../test-utils";
|
||||
import * as utils from "../../src/utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
import {CRYPTO_ENABLED} from "../../src/client";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
|
||||
let aliTestClient;
|
||||
const roomId = "!room:localhost";
|
||||
@@ -76,7 +75,7 @@ function expectAliQueryKeys() {
|
||||
);
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
return { device_keys: result };
|
||||
});
|
||||
return aliTestClient.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
@@ -104,7 +103,7 @@ function expectBobQueryKeys() {
|
||||
);
|
||||
const result = {};
|
||||
result[aliUserId] = aliKeys;
|
||||
return {device_keys: result};
|
||||
return { device_keys: result };
|
||||
});
|
||||
return bobTestClient.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
@@ -133,7 +132,7 @@ function expectAliClaimKeys() {
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
|
||||
return {one_time_keys: result};
|
||||
return { one_time_keys: result };
|
||||
});
|
||||
}).then(() => {
|
||||
// it can take a while to process the key query, so give it some extra
|
||||
@@ -145,7 +144,6 @@ function expectAliClaimKeys() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function aliDownloadsKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobTestClient.getSigningKey()).toBeTruthy();
|
||||
@@ -161,7 +159,7 @@ function aliDownloadsKeys() {
|
||||
// check that the localStorage is updated as we expect (not sure this is
|
||||
// an integration test, but meh)
|
||||
return Promise.all([p1, p2]).then(() => {
|
||||
return aliTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
return aliTestClient.client.crypto._deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const devices = data.devices[bobUserId];
|
||||
@@ -244,7 +242,7 @@ function bobSendsReplyMessage() {
|
||||
function expectAliSendMessageRequest() {
|
||||
return expectSendMessageRequest(aliTestClient.httpBackend).then(function(content) {
|
||||
aliMessages.push(content);
|
||||
expect(utils.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
|
||||
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
|
||||
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
@@ -261,7 +259,7 @@ function expectBobSendMessageRequest() {
|
||||
bobMessages.push(content);
|
||||
const aliKeyId = "curve25519:" + aliDeviceId;
|
||||
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
|
||||
expect(utils.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
||||
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
||||
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
@@ -270,7 +268,7 @@ function expectBobSendMessageRequest() {
|
||||
|
||||
function sendMessage(client) {
|
||||
return client.sendMessage(
|
||||
roomId, {msgtype: "m.text", body: "Hello, World"},
|
||||
roomId, { msgtype: "m.text", body: "Hello, World" },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -358,7 +356,6 @@ function recvMessage(httpBackend, client, sender, message) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send an initial sync response to the client (which just includes the member
|
||||
* list for our test room).
|
||||
@@ -396,7 +393,6 @@ function firstSync(testClient) {
|
||||
return testClient.flushSync();
|
||||
}
|
||||
|
||||
|
||||
describe("MatrixClient crypto", function() {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
@@ -478,7 +474,7 @@ describe("MatrixClient crypto", function() {
|
||||
).respond(200, function(path, content) {
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
return { device_keys: result };
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
@@ -520,7 +516,7 @@ describe("MatrixClient crypto", function() {
|
||||
).respond(200, function(path, content) {
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
return { device_keys: result };
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
@@ -534,7 +530,6 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("Bob starts his client and uploads device keys and one-time keys", function() {
|
||||
return Promise.resolve()
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -546,7 +541,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Ali sends a message", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -556,7 +551,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob receives a message", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -567,7 +562,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob receives a message with a bogus sender", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -621,7 +616,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Ali blocks Bob's device", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -641,7 +636,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob receives two pre-key messages", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -654,8 +649,8 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob replies to the message", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
bobTestClient.expectKeyQuery({device_keys: {[bobUserId]: {}}});
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -673,7 +668,7 @@ describe("MatrixClient crypto", function() {
|
||||
it("Ali does a key query when encryption is enabled", function() {
|
||||
// enabling encryption in the room should make alice download devices
|
||||
// for both members.
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
let client;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {EventTimeline} from "../../src/matrix";
|
||||
import {logger} from "../../src/logger";
|
||||
import {TestClient} from "../TestClient";
|
||||
import { EventTimeline } from "../../src/matrix";
|
||||
import { logger } from "../../src/logger";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
@@ -127,7 +127,7 @@ describe("getEventTimeline support", function() {
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{timelineSupport: true},
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
@@ -141,7 +141,6 @@ describe("getEventTimeline support", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("scrollback should be able to scroll back to before a gappy /sync",
|
||||
function() {
|
||||
// need a client with timelineSupport disabled to make this work
|
||||
@@ -218,7 +217,7 @@ describe("MatrixClient event timelines", function() {
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{timelineSupport: true},
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
@@ -516,7 +515,7 @@ describe("MatrixClient event timelines", function() {
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(tl, {backwards: true});
|
||||
return client.paginateEventTimeline(tl, { backwards: true });
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
@@ -532,7 +531,6 @@ describe("MatrixClient event timelines", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it("should allow you to paginate forwards", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
@@ -569,7 +567,7 @@ describe("MatrixClient event timelines", function() {
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(
|
||||
tl, {backwards: false, limit: 20});
|
||||
tl, { backwards: false, limit: 20 });
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
@@ -591,7 +589,7 @@ describe("MatrixClient event timelines", function() {
|
||||
const event = utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "a body",
|
||||
});
|
||||
event.unsigned = {transaction_id: TXN_ID};
|
||||
event.unsigned = { transaction_id: TXN_ID };
|
||||
|
||||
beforeEach(function() {
|
||||
// set up handlers for both the message send, and the
|
||||
@@ -680,7 +678,6 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should handle gappy syncs after redactions", function() {
|
||||
// https://github.com/vector-im/vector-web/issues/1389
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {CRYPTO_ENABLED} from "../../src/client";
|
||||
import {Filter, MemoryStore, Room} from "../../src/matrix";
|
||||
import {TestClient} from "../TestClient";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { Filter, MemoryStore, Room } from "../../src/matrix";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
let client = null;
|
||||
@@ -285,7 +285,6 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("downloadKeys", function() {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
@@ -337,7 +336,7 @@ describe("MatrixClient", function() {
|
||||
var b = JSON.parse(JSON.stringify(o));
|
||||
delete(b.signatures);
|
||||
delete(b.unsigned);
|
||||
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
|
||||
return client.crypto._olmDevice.sign(anotherjson.stringify(b));
|
||||
};
|
||||
|
||||
logger.log("Ed25519: " + ed25519key);
|
||||
@@ -346,10 +345,10 @@ describe("MatrixClient", function() {
|
||||
*/
|
||||
|
||||
httpBackend.when("POST", "/keys/query").check(function(req) {
|
||||
expect(req.data).toEqual({device_keys: {
|
||||
expect(req.data).toEqual({ device_keys: {
|
||||
'boris': [],
|
||||
'chaz': [],
|
||||
}});
|
||||
} });
|
||||
}).respond(200, {
|
||||
device_keys: {
|
||||
boris: borisKeys,
|
||||
@@ -379,12 +378,12 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
describe("deleteDevice", function() {
|
||||
const auth = {a: 1};
|
||||
const auth = { a: 1 };
|
||||
it("should pass through an auth dict", function() {
|
||||
httpBackend.when(
|
||||
"DELETE", "/_matrix/client/r0/devices/my_device",
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual({auth: auth});
|
||||
expect(req.data).toEqual({ auth: auth });
|
||||
}).respond(200);
|
||||
|
||||
const prom = client.deleteDevice("my_device", auth);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as utils from "../test-utils";
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
import {MatrixClient} from "../../src/matrix";
|
||||
import {MatrixScheduler} from "../../src/scheduler";
|
||||
import {MemoryStore} from "../../src/store/memory";
|
||||
import {MatrixError} from "../../src/http-api";
|
||||
import { MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { MemoryStore } from "../../src/store/memory";
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {EventStatus} from "../../src/matrix";
|
||||
import {MatrixScheduler} from "../../src/scheduler";
|
||||
import {Room} from "../../src/models/room";
|
||||
import {TestClient} from "../TestClient";
|
||||
import { EventStatus } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
let client = null;
|
||||
@@ -19,7 +19,7 @@ describe("MatrixClient retrying", function() {
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{scheduler},
|
||||
{ scheduler },
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {EventStatus} from "../../src/models/event";
|
||||
import {TestClient} from "../TestClient";
|
||||
|
||||
import { EventStatus } from "../../src/models/event";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient room timelines", function() {
|
||||
let client = null;
|
||||
@@ -104,7 +103,7 @@ describe("MatrixClient room timelines", function() {
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{timelineSupport: true},
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
@@ -166,7 +165,7 @@ describe("MatrixClient room timelines", function() {
|
||||
body: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = {transaction_id: "txn1"};
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
@@ -198,7 +197,7 @@ describe("MatrixClient room timelines", function() {
|
||||
body: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = {transaction_id: "txn1"};
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
@@ -396,8 +395,8 @@ describe("MatrixClient room timelines", function() {
|
||||
describe("new events", function() {
|
||||
it("should be added to the right place in the timeline", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
|
||||
@@ -434,11 +433,11 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
it("should set the right event.sender values", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
utils.mkMembership({
|
||||
user: userId, room: roomId, mship: "join", name: "New Name",
|
||||
}),
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
|
||||
setNextSyncData(eventData);
|
||||
@@ -546,7 +545,7 @@ describe("MatrixClient room timelines", function() {
|
||||
describe("gappy sync", function() {
|
||||
it("should copy the last known state to the new timeline", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
@@ -579,7 +578,7 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
it("should emit a 'Room.timelineReset' event", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {MatrixEvent} from "../../src/models/event";
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import * as utils from "../test-utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient syncing", function() {
|
||||
let client = null;
|
||||
@@ -122,7 +122,6 @@ describe("MatrixClient syncing", function() {
|
||||
resolveInvitesToProfiles: true,
|
||||
});
|
||||
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
@@ -677,8 +676,8 @@ describe("MatrixClient syncing", function() {
|
||||
it("should create and use an appropriate filter", function() {
|
||||
httpBackend.when("POST", "/filter").check(function(req) {
|
||||
expect(req.data).toEqual({
|
||||
room: { timeline: {limit: 1},
|
||||
include_leave: true }});
|
||||
room: { timeline: { limit: 1 },
|
||||
include_leave: true } });
|
||||
}).respond(200, { filter_id: "another_id" });
|
||||
|
||||
const prom = new Promise((resolve) => {
|
||||
|
||||
@@ -16,10 +16,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import * as utils from "../../src/utils";
|
||||
import * as testUtils from "../test-utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
import {logger} from "../../src/logger";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { logger } from "../../src/logger";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -32,7 +31,7 @@ const ROOM_ID = "!room:id";
|
||||
*/
|
||||
function createOlmSession(olmAccount, recipientTestClient) {
|
||||
return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => {
|
||||
const otkId = utils.keys(keys)[0];
|
||||
const otkId = Object.keys(keys)[0];
|
||||
const otk = keys[otkId];
|
||||
|
||||
const session = new global.Olm.Session();
|
||||
@@ -197,7 +196,6 @@ function getSyncResponse(roomMembers) {
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
|
||||
describe("megolm", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('not running megolm tests: Olm not present');
|
||||
@@ -257,7 +255,7 @@ describe("megolm", function() {
|
||||
const testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
|
||||
testOlmAccount.mark_keys_as_published();
|
||||
|
||||
const keyId = utils.keys(testOneTimeKeys.curve25519)[0];
|
||||
const keyId = Object.keys(testOneTimeKeys.curve25519)[0];
|
||||
const oneTimeKey = testOneTimeKeys.curve25519[keyId];
|
||||
const keyResult = {
|
||||
'key': oneTimeKey,
|
||||
@@ -269,7 +267,7 @@ describe("megolm", function() {
|
||||
'ed25519:DEVICE_ID': sig,
|
||||
};
|
||||
|
||||
const claimResponse = {one_time_keys: {}};
|
||||
const claimResponse = { one_time_keys: {} };
|
||||
claimResponse.one_time_keys[userId] = {
|
||||
'DEVICE_ID': {},
|
||||
};
|
||||
@@ -484,8 +482,9 @@ describe("megolm", function() {
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(function() {
|
||||
}).then(async function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
await room.decryptCriticalEvents();
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
@@ -494,7 +493,7 @@ describe("megolm", function() {
|
||||
it('Alice sends a megolm message', function() {
|
||||
let p2pSession;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -577,7 +576,7 @@ describe("megolm", function() {
|
||||
});
|
||||
|
||||
it("We shouldn't attempt to send to blocked devices", function() {
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -634,7 +633,7 @@ describe("megolm", function() {
|
||||
let p2pSession;
|
||||
let megolmSessionId;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -841,13 +840,12 @@ describe("megolm", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('Alice should wait for device list to complete when sending a megolm message',
|
||||
function() {
|
||||
let downloadPromise;
|
||||
let sendPromise;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -887,11 +885,10 @@ describe("megolm", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("Alice exports megolm keys and imports them to a new device", function() {
|
||||
let messageEncrypted;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -933,8 +930,9 @@ describe("megolm", function() {
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
}).then(async function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
await room.decryptCriticalEvents();
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
|
||||
@@ -971,4 +969,60 @@ describe("megolm", function() {
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
});
|
||||
|
||||
it("Alice receives an untrusted megolm key, only to receive the trusted one shortly after", function() {
|
||||
const testClient = new TestClient(
|
||||
"@alice:localhost", "device2", "access_token2",
|
||||
);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(groupSession.session_key());
|
||||
const rawEvent = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
return testClient.client.initCrypto().then(() => {
|
||||
const keys = [{
|
||||
room_id: ROOM_ID,
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
session_id: groupSession.session_id(),
|
||||
session_key: inboundGroupSession.export_session(0),
|
||||
sender_key: testSenderKey,
|
||||
}];
|
||||
return testClient.client.importRoomKeys(keys, { untrusted: true });
|
||||
}).then(() => {
|
||||
const event = testUtils.mkEvent({
|
||||
event: true,
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
return event.attemptDecryption(testClient.client.crypto, true).then(() => {
|
||||
expect(event.isKeySourceUntrusted()).toBeTruthy();
|
||||
});
|
||||
}).then(() => {
|
||||
const event = testUtils.mkEvent({
|
||||
type: 'm.room_key',
|
||||
content: {
|
||||
room_id: ROOM_ID,
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
session_id: groupSession.session_id(),
|
||||
session_key: groupSession.session_key(),
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
event._senderCurve25519Key = testSenderKey;
|
||||
return testClient.client.crypto._onRoomKeyEvent(event);
|
||||
}).then(() => {
|
||||
const event = testUtils.mkEvent({
|
||||
event: true,
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
return event.attemptDecryption(testClient.client.crypto, true).then(() => {
|
||||
expect(event.isKeySourceUntrusted()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from '../src/logger';
|
||||
import { logger } from '../src/logger';
|
||||
import * as utils from "../src/utils";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
global.Olm = require('@matrix-org/olm');
|
||||
logger.log('loaded libolm');
|
||||
} catch (e) {
|
||||
logger.warn("unable to run crypto tests: libolm not available");
|
||||
|
||||
+20
-20
@@ -1,8 +1,8 @@
|
||||
// load olm before the sdk if possible
|
||||
import './olm-loader';
|
||||
|
||||
import {logger} from '../src/logger';
|
||||
import {MatrixEvent} from "../src/models/event";
|
||||
import { logger } from '../src/logger';
|
||||
import { MatrixEvent } from "../src/models/event";
|
||||
|
||||
/**
|
||||
* Return a promise that is resolved when the client next emits a
|
||||
@@ -177,7 +177,6 @@ export function mkMessage(opts) {
|
||||
return mkEvent(opts);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A mock implementation of webstorage
|
||||
*
|
||||
@@ -204,7 +203,6 @@ MockStorageApi.prototype = {
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* If an event is being decrypted, wait for it to finish being decrypted.
|
||||
*
|
||||
@@ -212,21 +210,23 @@ MockStorageApi.prototype = {
|
||||
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
|
||||
*/
|
||||
export function awaitDecryption(event) {
|
||||
if (!event.isBeingDecrypted()) {
|
||||
return Promise.resolve(event);
|
||||
}
|
||||
// An event is not always decrypted ahead of time
|
||||
// getClearContent is a good signal to know whether an event has been decrypted
|
||||
// already
|
||||
if (event.getClearContent() !== null) {
|
||||
return event;
|
||||
} else {
|
||||
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
||||
|
||||
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function HttpResponse(
|
||||
httpLookups, acceptKeepalives, ignoreUnhandledSync,
|
||||
) {
|
||||
@@ -357,12 +357,12 @@ export function setHttpResponses(
|
||||
);
|
||||
|
||||
const httpReq = httpResponseObj.request.bind(httpResponseObj);
|
||||
client._http = [
|
||||
client.http = [
|
||||
"authedRequest", "authedRequestWithPrefix", "getContentUri",
|
||||
"request", "requestWithPrefix", "uploadContent",
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
client._http.authedRequest.mockImplementation(httpReq);
|
||||
client._http.authedRequestWithPrefix.mockImplementation(httpReq);
|
||||
client._http.requestWithPrefix.mockImplementation(httpReq);
|
||||
client._http.request.mockImplementation(httpReq);
|
||||
client.http.authedRequest.mockImplementation(httpReq);
|
||||
client.http.authedRequestWithPrefix.mockImplementation(httpReq);
|
||||
client.http.requestWithPrefix.mockImplementation(httpReq);
|
||||
client.http.request.mockImplementation(httpReq);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Copyright 2021 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 { NamespacedValue, UnstableValue } from "../../src/NamespacedValue";
|
||||
|
||||
describe("NamespacedValue", () => {
|
||||
it("should prefer stable over unstable", () => {
|
||||
const ns = new NamespacedValue("stable", "unstable");
|
||||
expect(ns.name).toBe(ns.stable);
|
||||
expect(ns.altName).toBe(ns.unstable);
|
||||
});
|
||||
|
||||
it("should return unstable if there is no stable", () => {
|
||||
const ns = new NamespacedValue(null, "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should have a falsey unstable if needed", () => {
|
||||
const ns = new NamespacedValue("stable", null);
|
||||
expect(ns.name).toBe(ns.stable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should match against either stable or unstable", () => {
|
||||
const ns = new NamespacedValue("stable", "unstable");
|
||||
expect(ns.matches("no")).toBe(false);
|
||||
expect(ns.matches(ns.stable)).toBe(true);
|
||||
expect(ns.matches(ns.unstable)).toBe(true);
|
||||
});
|
||||
|
||||
it("should not permit falsey values for both parts", () => {
|
||||
try {
|
||||
new UnstableValue(null, null);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("One of stable or unstable values must be supplied");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("UnstableValue", () => {
|
||||
it("should prefer unstable over stable", () => {
|
||||
const ns = new UnstableValue("stable", "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBe(ns.stable);
|
||||
});
|
||||
|
||||
it("should return unstable if there is no stable", () => {
|
||||
const ns = new UnstableValue(null, "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not permit falsey unstable values", () => {
|
||||
try {
|
||||
new UnstableValue("stable", null);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("Unstable value must be supplied");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2021 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 { EventEmitter } from "events";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
|
||||
const EVENTNAME = "UnknownEntry";
|
||||
|
||||
class EventSource extends EventEmitter {
|
||||
doTheThing() {
|
||||
this.emit(EVENTNAME, "foo", "bar");
|
||||
}
|
||||
|
||||
doAnError() {
|
||||
this.emit('error');
|
||||
}
|
||||
}
|
||||
|
||||
class EventTarget extends EventEmitter {
|
||||
|
||||
}
|
||||
|
||||
describe("ReEmitter", function() {
|
||||
it("Re-Emits events with the same args", function() {
|
||||
const src = new EventSource();
|
||||
const tgt = new EventTarget();
|
||||
|
||||
const handler = jest.fn();
|
||||
tgt.on(EVENTNAME, handler);
|
||||
|
||||
const reEmitter = new ReEmitter(tgt);
|
||||
reEmitter.reEmit(src, [EVENTNAME]);
|
||||
|
||||
src.doTheThing();
|
||||
|
||||
// Args should be the args passed to 'emit' after the event name, and
|
||||
// also the source object of the event which re-emitter adds
|
||||
expect(handler).toHaveBeenCalledWith("foo", "bar", src);
|
||||
});
|
||||
|
||||
it("Doesn't throw if no handler for 'error' event", function() {
|
||||
const src = new EventSource();
|
||||
const tgt = new EventTarget();
|
||||
|
||||
const reEmitter = new ReEmitter(tgt);
|
||||
reEmitter.reEmit(src, ['error']);
|
||||
|
||||
// without the workaround in ReEmitter, this would throw
|
||||
src.doAnError();
|
||||
|
||||
const handler = jest.fn();
|
||||
tgt.on('error', handler);
|
||||
|
||||
src.doAnError();
|
||||
|
||||
// Now we've attached an error handler, it should be called
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
import * as sdk from "../../src";
|
||||
import {AutoDiscovery} from "../../src/autodiscovery";
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
|
||||
describe("AutoDiscovery", function() {
|
||||
let httpBackend = null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {getHttpUriForMxc} from "../../src/content-repo";
|
||||
import { getHttpUriForMxc } from "../../src/content-repo";
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
const baseUrl = "https://my.home.server";
|
||||
|
||||
+20
-20
@@ -1,16 +1,16 @@
|
||||
import '../olm-loader';
|
||||
import {Crypto} from "../../src/crypto";
|
||||
import {WebStorageSessionStore} from "../../src/store/session/webstorage";
|
||||
import {MemoryCryptoStore} from "../../src/crypto/store/memory-crypto-store";
|
||||
import {MockStorageApi} from "../MockStorageApi";
|
||||
import {TestClient} from "../TestClient";
|
||||
import {MatrixEvent} from "../../src/models/event";
|
||||
import {Room} from "../../src/models/room";
|
||||
import { Crypto } from "../../src/crypto";
|
||||
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
|
||||
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
|
||||
import { MockStorageApi } from "../MockStorageApi";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import * as olmlib from "../../src/crypto/olmlib";
|
||||
import {sleep} from "../../src/utils";
|
||||
import {EventEmitter} from "events";
|
||||
import {CRYPTO_ENABLED} from "../../src/client";
|
||||
import {DeviceInfo} from "../../src/crypto/deviceinfo";
|
||||
import { sleep } from "../../src/utils";
|
||||
import { EventEmitter } from "events";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("Crypto", function() {
|
||||
|
||||
// unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
|
||||
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
event.getWireContent = () => {return {algorithm: olmlib.MEGOLM_ALGORITHM};};
|
||||
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
|
||||
event.getForwardingCurve25519KeyChain = () => ["not empty"];
|
||||
event.isKeySourceUntrusted = () => false;
|
||||
event.getClaimedEd25519Key =
|
||||
@@ -65,7 +65,7 @@ describe("Crypto", function() {
|
||||
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
device.keys["ed25519:FLIBBLE"] =
|
||||
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
client._crypto._deviceList.getDeviceByIdentityKey = () => device;
|
||||
client.crypto._deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
@@ -213,7 +213,7 @@ describe("Crypto", function() {
|
||||
|
||||
async function keyshareEventForEvent(event, index) {
|
||||
const eventContent = event.getWireContent();
|
||||
const key = await aliceClient._crypto._olmDevice
|
||||
const key = await aliceClient.crypto._olmDevice
|
||||
.getInboundGroupSessionKey(
|
||||
roomId, eventContent.sender_key, eventContent.session_id,
|
||||
index,
|
||||
@@ -273,19 +273,19 @@ describe("Crypto", function() {
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient._crypto.encryptEvent(event, aliceRoom);
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
event._clearEvent = {};
|
||||
event._senderCurve25519Key = null;
|
||||
event._claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient._crypto.decryptEvent(event);
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const bobDecryptor = bobClient._crypto._getRoomDecryptor(
|
||||
const bobDecryptor = bobClient.crypto._getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
@@ -302,7 +302,7 @@ describe("Crypto", function() {
|
||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
|
||||
const cryptoStore = bobClient._cryptoStore;
|
||||
const cryptoStore = bobClient.cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
@@ -344,7 +344,7 @@ describe("Crypto", function() {
|
||||
},
|
||||
});
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
const cryptoStore = aliceClient._cryptoStore;
|
||||
const cryptoStore = aliceClient.cryptoStore;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: "!someroom",
|
||||
@@ -377,7 +377,7 @@ describe("Crypto", function() {
|
||||
// key requests get queued until the sync has finished, but we don't
|
||||
// let the client set up enough for that to happen, so gut-wrench a bit
|
||||
// to force it to send now.
|
||||
aliceClient._crypto._outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||
aliceClient.crypto._outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||
jest.runAllTimers();
|
||||
await Promise.resolve();
|
||||
expect(aliceClient.sendToDevice).toBeCalledTimes(1);
|
||||
|
||||
@@ -22,11 +22,11 @@ import {
|
||||
import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store';
|
||||
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
import {OlmDevice} from "../../../src/crypto/OlmDevice";
|
||||
import {logger} from '../../../src/logger';
|
||||
import { OlmDevice } from "../../../src/crypto/OlmDevice";
|
||||
import { logger } from '../../../src/logger';
|
||||
|
||||
const userId = "@alice:example.com";
|
||||
|
||||
@@ -66,7 +66,7 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
});
|
||||
|
||||
it.each(types)("should throw if the callback returns falsey",
|
||||
async ({type, shouldCache}) => {
|
||||
async ({ type, shouldCache }) => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: () => false,
|
||||
});
|
||||
|
||||
@@ -16,10 +16,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from "../../../src/logger";
|
||||
import { logger } from "../../../src/logger";
|
||||
import * as utils from "../../../src/utils";
|
||||
import {MemoryCryptoStore} from "../../../src/crypto/store/memory-crypto-store";
|
||||
import {DeviceList} from "../../../src/crypto/DeviceList";
|
||||
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
|
||||
import { DeviceList } from "../../../src/crypto/DeviceList";
|
||||
|
||||
const signedDeviceList = {
|
||||
"failures": {},
|
||||
@@ -51,6 +51,36 @@ const signedDeviceList = {
|
||||
},
|
||||
};
|
||||
|
||||
const signedDeviceList2 = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
"@test2:sw1v.org": {
|
||||
"QJVRHWAKGH": {
|
||||
"signatures": {
|
||||
"@test2:sw1v.org": {
|
||||
"ed25519:QJVRHWAKGH":
|
||||
"w1xxdLe1iIqzEFHLRVYQeuiM6t2N2ZRiI8s5nDKxf054BP8" +
|
||||
"1CPEX/AQXh5BhkKAVMlKnwg4T9zU1/wBALeajk3",
|
||||
},
|
||||
},
|
||||
"user_id": "@test2:sw1v.org",
|
||||
"keys": {
|
||||
"ed25519:QJVRHWAKGH":
|
||||
"Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
|
||||
"curve25519:QJVRHWAKGH":
|
||||
"YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
|
||||
},
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "QJVRHWAKGH",
|
||||
"unsigned": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('DeviceList', function() {
|
||||
let downloadSpy;
|
||||
let cryptoStore;
|
||||
@@ -69,7 +99,7 @@ describe('DeviceList', function() {
|
||||
}
|
||||
});
|
||||
|
||||
function createTestDeviceList() {
|
||||
function createTestDeviceList(keyDownloadChunkSize = 250) {
|
||||
const baseApis = {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
getUserId: () => '@test1:sw1v.org',
|
||||
@@ -78,7 +108,7 @@ describe('DeviceList', function() {
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
};
|
||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm);
|
||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
|
||||
deviceLists.push(dl);
|
||||
return dl;
|
||||
}
|
||||
@@ -150,4 +180,30 @@ describe('DeviceList', function() {
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
});
|
||||
});
|
||||
|
||||
it("should download device keys in batches", function() {
|
||||
const dl = createTestDeviceList(1);
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
dl.startTrackingDeviceList('@test2:sw1v.org');
|
||||
|
||||
const queryDefer1 = utils.defer();
|
||||
downloadSpy.mockReturnValueOnce(queryDefer1.promise);
|
||||
const queryDefer2 = utils.defer();
|
||||
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toBeCalledTimes(2);
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(1, ['@test1:sw1v.org'], {});
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(2, ['@test2:sw1v.org'], {});
|
||||
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
|
||||
queryDefer2.resolve(utils.deepCopy(signedDeviceList2));
|
||||
|
||||
return prom1.then(() => {
|
||||
const storedKeys1 = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
|
||||
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
|
||||
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import '../../../olm-loader';
|
||||
import * as algorithms from "../../../../src/crypto/algorithms";
|
||||
import {MemoryCryptoStore} from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import {MockStorageApi} from "../../../MockStorageApi";
|
||||
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import { MockStorageApi } from "../../../MockStorageApi";
|
||||
import * as testUtils from "../../../test-utils";
|
||||
import {OlmDevice} from "../../../../src/crypto/OlmDevice";
|
||||
import {Crypto} from "../../../../src/crypto";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import {TestClient} from "../../../TestClient";
|
||||
import {Room} from "../../../../src/models/room";
|
||||
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
|
||||
import { Crypto } from "../../../../src/crypto";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { TestClient } from "../../../TestClient";
|
||||
import { Room } from "../../../../src/models/room";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
@@ -50,7 +50,6 @@ describe("MegolmDecryption", function() {
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
|
||||
// we stub out the olm encryption bits
|
||||
mockOlmLib = {};
|
||||
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
|
||||
@@ -136,9 +135,9 @@ describe("MegolmDecryption", function() {
|
||||
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
|
||||
|
||||
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({
|
||||
'@alice:foo': {'alidevice': {
|
||||
'@alice:foo': { 'alidevice': {
|
||||
sessionId: 'alisession',
|
||||
}},
|
||||
} },
|
||||
});
|
||||
|
||||
const awaitEncryptForDevice = new Promise((res, rej) => {
|
||||
@@ -258,6 +257,9 @@ describe("MegolmDecryption", function() {
|
||||
});
|
||||
|
||||
it("re-uses sessions for sequential messages", async function() {
|
||||
mockCrypto._backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
@@ -313,7 +315,7 @@ describe("MegolmDecryption", function() {
|
||||
});
|
||||
const mockRoom = {
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue(
|
||||
[{userId: "@alice:home.server"}],
|
||||
[{ userId: "@alice:home.server" }],
|
||||
),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
@@ -363,9 +365,9 @@ describe("MegolmDecryption", function() {
|
||||
bobClient1.initCrypto(),
|
||||
bobClient2.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient._crypto._olmDevice;
|
||||
const bobDevice1 = bobClient1._crypto._olmDevice;
|
||||
const bobDevice2 = bobClient2._crypto._olmDevice;
|
||||
const aliceDevice = aliceClient.crypto._olmDevice;
|
||||
const bobDevice1 = bobClient1.crypto._olmDevice;
|
||||
const bobDevice2 = bobClient2.crypto._olmDevice;
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -373,7 +375,7 @@ describe("MegolmDecryption", function() {
|
||||
const roomId = "!someroom";
|
||||
const room = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
room.getEncryptionTargetMembers = async function() {
|
||||
return [{userId: "@bob:example.com"}];
|
||||
return [{ userId: "@bob:example.com" }];
|
||||
};
|
||||
room.setBlacklistUnverifiedDevices(true);
|
||||
aliceClient.store.storeRoom(room);
|
||||
@@ -402,10 +404,10 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient._crypto._deviceList.storeDevicesForUser(
|
||||
aliceClient.crypto._deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
|
||||
aliceClient.crypto._deviceList.downloadKeys = async function(userIds) {
|
||||
return this._getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
@@ -446,7 +448,7 @@ describe("MegolmDecryption", function() {
|
||||
body: "secret",
|
||||
},
|
||||
});
|
||||
await aliceClient._crypto.encryptEvent(event, room);
|
||||
await aliceClient.crypto.encryptEvent(event, room);
|
||||
|
||||
expect(run).toBe(true);
|
||||
|
||||
@@ -466,8 +468,8 @@ describe("MegolmDecryption", function() {
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient._crypto._olmDevice;
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
const aliceDevice = aliceClient.crypto._olmDevice;
|
||||
const bobDevice = bobClient.crypto._olmDevice;
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -506,10 +508,10 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient._crypto._deviceList.storeDevicesForUser(
|
||||
aliceClient.crypto._deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
|
||||
aliceClient.crypto._deviceList.downloadKeys = async function(userIds) {
|
||||
return this._getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
@@ -544,7 +546,7 @@ describe("MegolmDecryption", function() {
|
||||
event_id: "$event",
|
||||
content: {},
|
||||
});
|
||||
await aliceClient._crypto.encryptEvent(event, aliceRoom);
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
await sendPromise;
|
||||
});
|
||||
|
||||
@@ -559,11 +561,11 @@ describe("MegolmDecryption", function() {
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
const bobDevice = bobClient.crypto._olmDevice;
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
||||
aliceClient.crypto._onToDeviceEvent(new MatrixEvent({
|
||||
type: "org.matrix.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
@@ -576,7 +578,7 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
@@ -602,14 +604,14 @@ describe("MegolmDecryption", function() {
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
aliceClient._crypto.downloadKeys = async () => {};
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
aliceClient.crypto.downloadKeys = async () => {};
|
||||
const bobDevice = bobClient.crypto._olmDevice;
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
||||
aliceClient.crypto._onToDeviceEvent(new MatrixEvent({
|
||||
type: "org.matrix.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
@@ -626,7 +628,7 @@ describe("MegolmDecryption", function() {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
@@ -653,15 +655,15 @@ describe("MegolmDecryption", function() {
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
aliceClient._crypto.downloadKeys = async () => {};
|
||||
const bobDevice = bobClient.crypto._olmDevice;
|
||||
aliceClient.crypto.downloadKeys = async () => {};
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// pretend we got an event that we can't decrypt
|
||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
||||
aliceClient.crypto._onToDeviceEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
@@ -676,7 +678,7 @@ describe("MegolmDecryption", function() {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
|
||||
@@ -16,12 +16,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../../olm-loader';
|
||||
import {MemoryCryptoStore} from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import {MockStorageApi} from "../../../MockStorageApi";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {OlmDevice} from "../../../../src/crypto/OlmDevice";
|
||||
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import { MockStorageApi } from "../../../MockStorageApi";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
|
||||
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
|
||||
|
||||
function makeOlmDevice() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
@@ -190,5 +190,91 @@ describe("OlmDevice", function() {
|
||||
// new session and will have called claimOneTimeKeys
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
|
||||
it("avoids deadlocks when two tasks are ensuring the same devices", async function() {
|
||||
// This test checks whether `ensureOlmSessionsForDevices` properly
|
||||
// handles multiple tasks in flight ensuring some set of devices in
|
||||
// common without deadlocks.
|
||||
|
||||
let claimRequestCount = 0;
|
||||
const baseApis = {
|
||||
claimOneTimeKeys: () => {
|
||||
// simulate a very slow server (.5 seconds to respond)
|
||||
claimRequestCount++;
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(reject, 500);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const deviceBobA = DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
"curve25519:BOB-A": "akey",
|
||||
},
|
||||
}, "BOB-A");
|
||||
const deviceBobB = DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
"curve25519:BOB-B": "bkey",
|
||||
},
|
||||
}, "BOB-B");
|
||||
|
||||
// There's no required ordering of devices per user, so here we
|
||||
// create two different orderings so that each task reserves a
|
||||
// device the other task needs before continuing.
|
||||
const devicesByUserAB = {
|
||||
"@bob:example.com": [
|
||||
deviceBobA,
|
||||
deviceBobB,
|
||||
],
|
||||
};
|
||||
const devicesByUserBA = {
|
||||
"@bob:example.com": [
|
||||
deviceBobB,
|
||||
deviceBobA,
|
||||
],
|
||||
};
|
||||
|
||||
function alwaysSucceed(promise) {
|
||||
// swallow any exception thrown by a promise, so that
|
||||
// Promise.all doesn't abort
|
||||
return promise.catch(() => {});
|
||||
}
|
||||
|
||||
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUserAB,
|
||||
));
|
||||
|
||||
// After a single tick through the first task, it should have
|
||||
// claimed ownership of all devices to avoid deadlocking others.
|
||||
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
|
||||
|
||||
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUserBA,
|
||||
));
|
||||
|
||||
// The second task should not have changed the ownership count, as
|
||||
// it's waiting on the first task.
|
||||
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
|
||||
|
||||
// Track the tasks, but don't await them yet.
|
||||
const promises = Promise.all([
|
||||
task1,
|
||||
task2,
|
||||
]);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 200);
|
||||
});
|
||||
|
||||
// After .2s, the first task should have made an initial claim request.
|
||||
expect(claimRequestCount).toBe(1);
|
||||
|
||||
await promises;
|
||||
|
||||
// After waiting for both tasks to complete, the first task should
|
||||
// have failed, so the second task should have tried to create a
|
||||
// new session and will have called claimOneTimeKeys
|
||||
expect(claimRequestCount).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,18 +16,19 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../olm-loader';
|
||||
import {logger} from "../../../src/logger";
|
||||
import { logger } from "../../../src/logger";
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {MatrixClient} from "../../../src/client";
|
||||
import {MatrixEvent} from "../../../src/models/event";
|
||||
import { MatrixClient } from "../../../src/client";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import * as algorithms from "../../../src/crypto/algorithms";
|
||||
import {WebStorageSessionStore} from "../../../src/store/session/webstorage";
|
||||
import {MemoryCryptoStore} from "../../../src/crypto/store/memory-crypto-store";
|
||||
import {MockStorageApi} from "../../MockStorageApi";
|
||||
import { WebStorageSessionStore } from "../../../src/store/session/webstorage";
|
||||
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
|
||||
import { MockStorageApi } from "../../MockStorageApi";
|
||||
import * as testUtils from "../../test-utils";
|
||||
import {OlmDevice} from "../../../src/crypto/OlmDevice";
|
||||
import {Crypto} from "../../../src/crypto";
|
||||
import {resetCrossSigningKeys} from "./crypto-utils";
|
||||
import { OlmDevice } from "../../../src/crypto/OlmDevice";
|
||||
import { Crypto } from "../../../src/crypto";
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { BackupManager } from "../../../src/crypto/backup";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -73,7 +74,7 @@ const KEY_BACKUP_DATA = {
|
||||
};
|
||||
|
||||
const BACKUP_INFO = {
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
@@ -138,6 +139,7 @@ describe("MegolmBackup", function() {
|
||||
let megolmDecryption;
|
||||
beforeEach(async function() {
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||
mockCrypto._backupManager = testUtils.mock(BackupManager, "BackupManager");
|
||||
mockCrypto.backupKey = new Olm.PkEncryption();
|
||||
mockCrypto.backupKey.set_recipient_key(
|
||||
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
@@ -215,12 +217,14 @@ describe("MegolmBackup", function() {
|
||||
};
|
||||
mockCrypto.cancelRoomKeyRequest = function() {};
|
||||
|
||||
mockCrypto.backupGroupSession = jest.fn();
|
||||
mockCrypto._backupManager = {
|
||||
backupGroupSession: jest.fn(),
|
||||
};
|
||||
|
||||
return event.attemptDecryption(mockCrypto).then(() => {
|
||||
return megolmDecryption.onRoomKeyEvent(event);
|
||||
}).then(() => {
|
||||
expect(mockCrypto.backupGroupSession).toHaveBeenCalled();
|
||||
expect(mockCrypto._backupManager.backupGroupSession).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -264,7 +268,7 @@ describe("MegolmBackup", function() {
|
||||
})
|
||||
.then(() => {
|
||||
client.enableKeyBackup({
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
@@ -272,7 +276,7 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
client._http.authedRequest = function(
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
@@ -292,12 +296,9 @@ describe("MegolmBackup", function() {
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
};
|
||||
client._crypto.backupGroupSession(
|
||||
"roomId",
|
||||
client.crypto._backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
[],
|
||||
groupSession.session_id(),
|
||||
groupSession.session_key(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(1);
|
||||
@@ -335,39 +336,48 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
await resetCrossSigningKeys(client);
|
||||
let numCalls = 0;
|
||||
await new Promise((resolve, reject) => {
|
||||
client._http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(1);
|
||||
if (numCalls >= 2) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
expect(method).toBe("POST");
|
||||
expect(path).toBe("/room_keys/version");
|
||||
try {
|
||||
// make sure auth_data is signed by the master key
|
||||
olmlib.pkVerify(
|
||||
data.auth_data, client.getCrossSigningId(), "@alice:bar",
|
||||
);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
return Promise.resolve({});
|
||||
}
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
};
|
||||
await Promise.all([
|
||||
new Promise((resolve, reject) => {
|
||||
let backupInfo;
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
if (numCalls === 1) {
|
||||
expect(method).toBe("POST");
|
||||
expect(path).toBe("/room_keys/version");
|
||||
try {
|
||||
// make sure auth_data is signed by the master key
|
||||
olmlib.pkVerify(
|
||||
data.auth_data, client.getCrossSigningId(), "@alice:bar",
|
||||
);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
return Promise.resolve({});
|
||||
}
|
||||
backupInfo = data;
|
||||
return Promise.resolve({});
|
||||
} else if (numCalls === 2) {
|
||||
expect(method).toBe("GET");
|
||||
expect(path).toBe("/room_keys/version");
|
||||
resolve();
|
||||
return Promise.resolve(backupInfo);
|
||||
} else {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many times"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
};
|
||||
}),
|
||||
client.createKeyBackupVersion({
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(numCalls).toBe(1);
|
||||
}),
|
||||
]);
|
||||
expect(numCalls).toBe(2);
|
||||
});
|
||||
|
||||
it('retries when a backup fails', function() {
|
||||
@@ -434,7 +444,7 @@ describe("MegolmBackup", function() {
|
||||
})
|
||||
.then(() => {
|
||||
client.enableKeyBackup({
|
||||
algorithm: "foobar",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
@@ -442,7 +452,7 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
client._http.authedRequest = function(
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
@@ -468,12 +478,9 @@ describe("MegolmBackup", function() {
|
||||
);
|
||||
}
|
||||
};
|
||||
client._crypto.backupGroupSession(
|
||||
"roomId",
|
||||
client.crypto._backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
[],
|
||||
groupSession.session_id(),
|
||||
groupSession.session_key(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(2);
|
||||
@@ -506,7 +513,7 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
|
||||
it('can restore from backup', function() {
|
||||
client._http.authedRequest = function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(KEY_BACKUP_DATA);
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
@@ -523,7 +530,7 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
|
||||
it('can restore backup by room', function() {
|
||||
client._http.authedRequest = function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve({
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
@@ -546,15 +553,15 @@ describe("MegolmBackup", function() {
|
||||
|
||||
it('has working cache functions', async function() {
|
||||
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
await client._crypto.storeSessionBackupPrivateKey(key);
|
||||
const result = await client._crypto.getSessionBackupPrivateKey();
|
||||
await client.crypto.storeSessionBackupPrivateKey(key);
|
||||
const result = await client.crypto.getSessionBackupPrivateKey();
|
||||
expect(new Uint8Array(result)).toEqual(key);
|
||||
});
|
||||
|
||||
it('caches session backup keys as it encounters them', async function() {
|
||||
const cachedNull = await client._crypto.getSessionBackupPrivateKey();
|
||||
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
|
||||
expect(cachedNull).toBeNull();
|
||||
client._http.authedRequest = function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(KEY_BACKUP_DATA);
|
||||
};
|
||||
await new Promise((resolve) => {
|
||||
@@ -566,8 +573,24 @@ describe("MegolmBackup", function() {
|
||||
{ cacheCompleteCallback: resolve },
|
||||
);
|
||||
});
|
||||
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
|
||||
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
|
||||
expect(cachedKey).not.toBeNull();
|
||||
});
|
||||
|
||||
it("fails if an known algorithm is used", async function() {
|
||||
const BAD_BACKUP_INFO = Object.assign({}, BACKUP_INFO, {
|
||||
algorithm: "this.algorithm.does.not.exist",
|
||||
});
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(KEY_BACKUP_DATA);
|
||||
};
|
||||
|
||||
await expect(client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
BAD_BACKUP_INFO,
|
||||
)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,11 +18,11 @@ limitations under the License.
|
||||
import '../../olm-loader';
|
||||
import anotherjson from 'another-json';
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {HttpResponse, setHttpResponses} from '../../test-utils';
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { HttpResponse, setHttpResponses } from '../../test-utils';
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { MatrixError } from '../../../src/http-api';
|
||||
import {logger} from '../../../src/logger';
|
||||
import { logger } from '../../../src/logger';
|
||||
|
||||
async function makeTestClient(userInfo, options, keys) {
|
||||
if (!keys) keys = {};
|
||||
@@ -60,12 +60,12 @@ describe("Cross Signing", function() {
|
||||
|
||||
it("should sign the master key with the device key", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = jest.fn(async (auth, keys) => {
|
||||
await olmlib.verifySignature(
|
||||
alice._crypto._olmDevice, keys.master_key, "@alice:example.com",
|
||||
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
|
||||
alice.crypto._olmDevice, keys.master_key, "@alice:example.com",
|
||||
"Osborne2", alice.crypto._olmDevice.deviceEd25519Key,
|
||||
);
|
||||
});
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
@@ -80,7 +80,7 @@ describe("Cross Signing", function() {
|
||||
|
||||
it("should abort bootstrap if device signing auth fails", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async (auth, keys) => {
|
||||
const errorResponse = {
|
||||
@@ -131,14 +131,14 @@ describe("Cross Signing", function() {
|
||||
|
||||
it("should upload a signature when a user is verified", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's device key
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -175,7 +175,7 @@ describe("Cross Signing", function() {
|
||||
]);
|
||||
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
// will be called to sign our own device
|
||||
@@ -193,30 +193,36 @@ describe("Cross Signing", function() {
|
||||
const keyChangePromise = new Promise((resolve, reject) => {
|
||||
alice.once("crossSigning.keysChanged", async (e) => {
|
||||
resolve(e);
|
||||
await alice.checkOwnCrossSigningTrust();
|
||||
await alice.checkOwnCrossSigningTrust({
|
||||
allowPrivateKeyRequests: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const uploadSigsPromise = new Promise((resolve, reject) => {
|
||||
alice.uploadKeySignatures = jest.fn(async (content) => {
|
||||
await olmlib.verifySignature(
|
||||
alice._crypto._olmDevice,
|
||||
content["@alice:example.com"][
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
|
||||
],
|
||||
"@alice:example.com",
|
||||
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
|
||||
);
|
||||
olmlib.pkVerify(
|
||||
content["@alice:example.com"]["Osborne2"],
|
||||
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
|
||||
"@alice:example.com",
|
||||
);
|
||||
resolve();
|
||||
try {
|
||||
await olmlib.verifySignature(
|
||||
alice.crypto._olmDevice,
|
||||
content["@alice:example.com"][
|
||||
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
|
||||
],
|
||||
"@alice:example.com",
|
||||
"Osborne2", alice.crypto._olmDevice.deviceEd25519Key,
|
||||
);
|
||||
olmlib.pkVerify(
|
||||
content["@alice:example.com"]["Osborne2"],
|
||||
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
|
||||
"@alice:example.com",
|
||||
);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
|
||||
const deviceInfo = alice.crypto._deviceList._devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -224,7 +230,7 @@ describe("Cross Signing", function() {
|
||||
};
|
||||
aliceDevice.keys = deviceInfo.keys;
|
||||
aliceDevice.algorithms = deviceInfo.algorithms;
|
||||
await alice._crypto._signObject(aliceDevice);
|
||||
await alice.crypto._signObject(aliceDevice);
|
||||
olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com");
|
||||
|
||||
// feed sync result that includes master key, ssk, device key
|
||||
@@ -326,7 +332,7 @@ describe("Cross Signing", function() {
|
||||
|
||||
it("should use trust chain to determine device verification", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
@@ -352,7 +358,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -381,7 +387,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobPubkey]: sig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
// Bob's device key should be TOFU
|
||||
@@ -411,12 +417,12 @@ describe("Cross Signing", function() {
|
||||
it("should trust signatures received from other devices", async function() {
|
||||
const aliceKeys = {};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
null,
|
||||
aliceKeys,
|
||||
);
|
||||
alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com");
|
||||
alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {};
|
||||
alice.crypto._deviceList.startTrackingDeviceList("@bob:example.com");
|
||||
alice.crypto._deviceList.stopTrackingAllDeviceLists = () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
|
||||
@@ -431,14 +437,14 @@ describe("Cross Signing", function() {
|
||||
]);
|
||||
|
||||
const keyChangePromise = new Promise((resolve, reject) => {
|
||||
alice._crypto._deviceList.once("userCrossSigningUpdated", (userId) => {
|
||||
alice.crypto._deviceList.once("userCrossSigningUpdated", (userId) => {
|
||||
if (userId === "@bob:example.com") {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
|
||||
const deviceInfo = alice.crypto._deviceList._devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -446,7 +452,7 @@ describe("Cross Signing", function() {
|
||||
};
|
||||
aliceDevice.keys = deviceInfo.keys;
|
||||
aliceDevice.algorithms = deviceInfo.algorithms;
|
||||
await alice._crypto._signObject(aliceDevice);
|
||||
await alice.crypto._signObject(aliceDevice);
|
||||
|
||||
const bobOlmAccount = new global.Olm.Account();
|
||||
bobOlmAccount.create();
|
||||
@@ -573,7 +579,7 @@ describe("Cross Signing", function() {
|
||||
|
||||
it("should dis-trust an unsigned device", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
@@ -600,7 +606,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -623,7 +629,7 @@ describe("Cross Signing", function() {
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
// Bob's device key should be untrusted
|
||||
@@ -642,7 +648,7 @@ describe("Cross Signing", function() {
|
||||
|
||||
it("should dis-trust a user when their ssk changes", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
@@ -667,7 +673,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -695,7 +701,7 @@ describe("Cross Signing", function() {
|
||||
bobDevice.signatures = {};
|
||||
bobDevice.signatures["@bob:example.com"] = {};
|
||||
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig;
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
// Alice verifies Bob's SSK
|
||||
@@ -727,7 +733,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey2]: sskSig2,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -764,7 +770,7 @@ describe("Cross Signing", function() {
|
||||
// Alice gets new signature for device
|
||||
const sig2 = bobSigning2.sign(bobDeviceString);
|
||||
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
|
||||
@@ -780,7 +786,7 @@ describe("Cross Signing", function() {
|
||||
let upgradeResolveFunc;
|
||||
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
shouldUpgradeDeviceVerifications: (verifs) => {
|
||||
@@ -792,27 +798,27 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
);
|
||||
const bob = await makeTestClient(
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
);
|
||||
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
// set Bob's cross-signing key
|
||||
await resetCrossSigningKeys(bob);
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: {
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
keys: {
|
||||
"curve25519:Dynabook": bob._crypto._olmDevice.deviceCurve25519Key,
|
||||
"ed25519:Dynabook": bob._crypto._olmDevice.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bob.crypto._olmDevice.deviceCurve25519Key,
|
||||
"ed25519:Dynabook": bob.crypto._olmDevice.deviceEd25519Key,
|
||||
},
|
||||
verified: 1,
|
||||
known: true,
|
||||
},
|
||||
});
|
||||
alice._crypto._deviceList.storeCrossSigningForUser(
|
||||
alice.crypto._deviceList.storeCrossSigningForUser(
|
||||
"@bob:example.com",
|
||||
bob._crypto._crossSigningInfo.toStorage(),
|
||||
bob.crypto._crossSigningInfo.toStorage(),
|
||||
);
|
||||
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
@@ -832,7 +838,7 @@ describe("Cross Signing", function() {
|
||||
expect(bobTrust.isTofu()).toBeTruthy();
|
||||
|
||||
// "forget" that Bob is trusted
|
||||
delete alice._crypto._deviceList._crossSigningInfo["@bob:example.com"]
|
||||
delete alice.crypto._deviceList._crossSigningInfo["@bob:example.com"]
|
||||
.keys.master.signatures["@alice:example.com"];
|
||||
|
||||
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
|
||||
@@ -842,9 +848,9 @@ describe("Cross Signing", function() {
|
||||
upgradePromise = new Promise((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
|
||||
alice.crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
|
||||
await new Promise((resolve) => {
|
||||
alice._crypto.on("userTrustStatusChanged", resolve);
|
||||
alice.crypto.on("userTrustStatusChanged", resolve);
|
||||
});
|
||||
await upgradePromise;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {IndexedDBCryptoStore} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
|
||||
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
|
||||
// needs to be phased out and replaced with bootstrapSecretStorage,
|
||||
// but that is doing too much extra stuff for it to be an easy transition.
|
||||
@@ -7,7 +6,7 @@ export async function resetCrossSigningKeys(client, {
|
||||
level,
|
||||
authUploadDeviceSigningKeys = async func => await func(),
|
||||
} = {}) {
|
||||
const crypto = client._crypto;
|
||||
const crypto = client.crypto;
|
||||
|
||||
const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys);
|
||||
try {
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store';
|
||||
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@ limitations under the License.
|
||||
|
||||
import '../../olm-loader';
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {SECRET_STORAGE_ALGORITHM_V1_AES} from "../../../src/crypto/SecretStorage";
|
||||
import {MatrixEvent} from "../../../src/models/event";
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {makeTestClients} from './verification/util';
|
||||
import {encryptAES} from "../../../src/crypto/aes";
|
||||
import {resetCrossSigningKeys, createSecretStorageKey} from "./crypto-utils";
|
||||
import {logger} from '../../../src/logger';
|
||||
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { makeTestClients } from './verification/util';
|
||||
import { encryptAES } from "../../../src/crypto/aes";
|
||||
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
|
||||
import { logger } from '../../../src/logger';
|
||||
|
||||
import * as utils from "../../../src/utils";
|
||||
|
||||
@@ -47,7 +47,7 @@ async function makeTestClient(userInfo, options) {
|
||||
await client.initCrypto();
|
||||
|
||||
// No need to download keys for these tests
|
||||
client._crypto.downloadKeys = async function() {};
|
||||
client.crypto.downloadKeys = async function() {};
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -91,7 +91,7 @@ describe("Secrets", function() {
|
||||
});
|
||||
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => signingKey,
|
||||
@@ -99,11 +99,11 @@ describe("Secrets", function() {
|
||||
},
|
||||
},
|
||||
);
|
||||
alice._crypto._crossSigningInfo.setKeys({
|
||||
alice.crypto._crossSigningInfo.setKeys({
|
||||
master: signingkeyInfo,
|
||||
});
|
||||
|
||||
const secretStorage = alice._crypto._secretStorage;
|
||||
const secretStorage = alice.crypto._secretStorage;
|
||||
|
||||
alice.setAccountData = async function(eventType, contents, callback) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
@@ -120,7 +120,7 @@ describe("Secrets", function() {
|
||||
const keyAccountData = {
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
};
|
||||
await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master');
|
||||
await alice.crypto._crossSigningInfo.signObject(keyAccountData, 'master');
|
||||
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
@@ -141,7 +141,7 @@ describe("Secrets", function() {
|
||||
|
||||
it("should throw if given a key that doesn't exist", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -155,7 +155,7 @@ describe("Secrets", function() {
|
||||
|
||||
it("should refuse to encrypt with zero keys", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -175,7 +175,7 @@ describe("Secrets", function() {
|
||||
|
||||
let keys = {};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => keys[t],
|
||||
@@ -194,7 +194,7 @@ describe("Secrets", function() {
|
||||
};
|
||||
resetCrossSigningKeys(alice);
|
||||
|
||||
const newKeyId = await alice.addSecretStorageKey(
|
||||
const { keyId: newKeyId } = await alice.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
);
|
||||
// we don't await on this because it waits for the event to come down the sync
|
||||
@@ -208,7 +208,7 @@ describe("Secrets", function() {
|
||||
|
||||
it("should refuse to encrypt if no keys given and no default key", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -221,24 +221,24 @@ describe("Secrets", function() {
|
||||
it("should request secrets from other clients", async function() {
|
||||
const [osborne2, vax] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@alice:example.com", deviceId: "VAX"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@alice:example.com", deviceId: "VAX" },
|
||||
],
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
onSecretRequested: e => {
|
||||
expect(e.name).toBe("foo");
|
||||
onSecretRequested: (userId, deviceId, requestId, secretName, deviceTrust) => {
|
||||
expect(secretName).toBe("foo");
|
||||
return "bar";
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const vaxDevice = vax.client._crypto._olmDevice;
|
||||
const osborne2Device = osborne2.client._crypto._olmDevice;
|
||||
const secretStorage = osborne2.client._crypto._secretStorage;
|
||||
const vaxDevice = vax.client.crypto._olmDevice;
|
||||
const osborne2Device = osborne2.client.crypto._olmDevice;
|
||||
const secretStorage = osborne2.client.crypto._secretStorage;
|
||||
|
||||
osborne2.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
osborne2.client.crypto._deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"VAX": {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "VAX",
|
||||
@@ -249,7 +249,7 @@ describe("Secrets", function() {
|
||||
},
|
||||
},
|
||||
});
|
||||
vax.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
vax.client.crypto._deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"Osborne2": {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Osborne2",
|
||||
@@ -265,7 +265,7 @@ describe("Secrets", function() {
|
||||
const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
|
||||
await osborne2Device.markKeysAsPublished();
|
||||
|
||||
await vax.client._crypto._olmDevice.createOutboundSession(
|
||||
await vax.client.crypto._olmDevice.createOutboundSession(
|
||||
osborne2Device.deviceCurve25519Key,
|
||||
Object.values(otks)[0],
|
||||
);
|
||||
@@ -334,8 +334,8 @@ describe("Secrets", function() {
|
||||
createSecretStorageKey,
|
||||
});
|
||||
|
||||
const crossSigning = bob._crypto._crossSigningInfo;
|
||||
const secretStorage = bob._crypto._secretStorage;
|
||||
const crossSigning = bob.crypto._crossSigningInfo;
|
||||
const secretStorage = bob.crypto._secretStorage;
|
||||
|
||||
expect(crossSigning.getId()).toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
@@ -376,10 +376,10 @@ describe("Secrets", function() {
|
||||
]);
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
bob._crypto.checkKeyBackup = async () => {};
|
||||
bob.crypto._backupManager.checkKeyBackup = async () => {};
|
||||
|
||||
const crossSigning = bob._crypto._crossSigningInfo;
|
||||
const secretStorage = bob._crypto._secretStorage;
|
||||
const crossSigning = bob.crypto._crossSigningInfo;
|
||||
const secretStorage = bob.crypto._secretStorage;
|
||||
|
||||
// Set up cross-signing keys from scratch with specific storage key
|
||||
await bob.bootstrapCrossSigning({
|
||||
@@ -394,7 +394,7 @@ describe("Secrets", function() {
|
||||
});
|
||||
|
||||
// Clear local cross-signing keys and read from secret storage
|
||||
bob._crypto._deviceList.storeCrossSigningForUser(
|
||||
bob.crypto._deviceList.storeCrossSigningForUser(
|
||||
"@bob:example.com",
|
||||
crossSigning.toStorage(),
|
||||
);
|
||||
@@ -419,12 +419,12 @@ describe("Secrets", function() {
|
||||
key_id: SSSSKey,
|
||||
};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: k => crossSigningKeys = k,
|
||||
getSecretStorageKey: ({keys}, name) => {
|
||||
getSecretStorageKey: ({ keys }, name) => {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (secretStorageKeys[keyId]) {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
@@ -458,7 +458,7 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.master",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -466,7 +466,7 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -474,12 +474,12 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
alice.crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -525,7 +525,7 @@ describe("Secrets", function() {
|
||||
await alice.bootstrapSecretStorage();
|
||||
|
||||
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
|
||||
.toEqual({key: "key_id"});
|
||||
.toEqual({ key: "key_id" });
|
||||
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")
|
||||
.getContent();
|
||||
expect(keyInfo.algorithm)
|
||||
@@ -550,12 +550,12 @@ describe("Secrets", function() {
|
||||
key_id: SSSSKey,
|
||||
};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: k => crossSigningKeys = k,
|
||||
getSecretStorageKey: ({keys}, name) => {
|
||||
getSecretStorageKey: ({ keys }, name) => {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (secretStorageKeys[keyId]) {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
@@ -587,7 +587,7 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.master",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -595,7 +595,7 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -603,7 +603,7 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -619,7 +619,7 @@ describe("Secrets", function() {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
alice.crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
|
||||
@@ -13,9 +13,9 @@ 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 {InRoomChannel} from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
|
||||
describe("InRoomChannel tests", function() {
|
||||
const ALICE = "@alice:hs.tld";
|
||||
|
||||
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import { logger } from "../../../../src/logger";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import {verificationMethods} from "../../../../src/crypto";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {SAS} from "../../../../src/crypto/verification/SAS";
|
||||
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
|
||||
import { verificationMethods } from "../../../../src/crypto";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -42,14 +42,14 @@ describe("verification request integration tests with crypto layer", function()
|
||||
it("should request and accept a verification", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice.client._crypto._deviceList.getRawStoredDevicesForUser = function() {
|
||||
alice.client.crypto._deviceList.getRawStoredDevicesForUser = function() {
|
||||
return {
|
||||
Dynabook: {
|
||||
keys: {
|
||||
|
||||
@@ -15,14 +15,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import {SAS} from "../../../../src/crypto/verification/SAS";
|
||||
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
|
||||
import {verificationMethods} from "../../../../src/crypto";
|
||||
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
|
||||
import { verificationMethods } from "../../../../src/crypto";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {resetCrossSigningKeys} from "../crypto-utils";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { resetCrossSigningKeys } from "../crypto-utils";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -79,16 +79,16 @@ describe("SAS verification", function() {
|
||||
beforeEach(async () => {
|
||||
[alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
|
||||
const aliceDevice = alice.client._crypto._olmDevice;
|
||||
const bobDevice = bob.client._crypto._olmDevice;
|
||||
const aliceDevice = alice.client.crypto._olmDevice;
|
||||
const bobDevice = bob.client.crypto._olmDevice;
|
||||
|
||||
ALICE_DEVICES = {
|
||||
Osborne2: {
|
||||
@@ -114,14 +114,14 @@ describe("SAS verification", function() {
|
||||
},
|
||||
};
|
||||
|
||||
alice.client._crypto._deviceList.storeDevicesForUser(
|
||||
alice.client.crypto._deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
bob.client._crypto._deviceList.storeDevicesForUser(
|
||||
bob.client.crypto._deviceList.storeDevicesForUser(
|
||||
"@alice:example.com", ALICE_DEVICES,
|
||||
);
|
||||
bob.client.downloadKeys = () => {
|
||||
@@ -296,9 +296,9 @@ describe("SAS verification", function() {
|
||||
|
||||
await resetCrossSigningKeys(bob.client);
|
||||
|
||||
bob.client._crypto._deviceList.storeCrossSigningForUser(
|
||||
bob.client.crypto._deviceList.storeCrossSigningForUser(
|
||||
"@alice:example.com", {
|
||||
keys: alice.client._crypto._crossSigningInfo.keys,
|
||||
keys: alice.client.crypto._crossSigningInfo.keys,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -336,8 +336,8 @@ describe("SAS verification", function() {
|
||||
it("should send a cancellation message on error", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
@@ -390,8 +390,8 @@ describe("SAS verification", function() {
|
||||
beforeEach(async function() {
|
||||
[alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
|
||||
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {VerificationBase} from '../../../../src/crypto/verification/Base';
|
||||
import {CrossSigningInfo} from '../../../../src/crypto/CrossSigning';
|
||||
import {encodeBase64} from "../../../../src/crypto/olmlib";
|
||||
import {setupWebcrypto, teardownWebcrypto} from './util';
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
|
||||
import { encodeBase64 } from "../../../../src/crypto/olmlib";
|
||||
import { setupWebcrypto, teardownWebcrypto } from './util';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -69,7 +69,7 @@ describe("self-verifications", () => {
|
||||
const restoreKeyBackupWithCache = jest.fn(() => Promise.resolve());
|
||||
|
||||
const client = {
|
||||
_crypto: {
|
||||
crypto: {
|
||||
_crossSigningInfo,
|
||||
_secretStorage,
|
||||
storeSessionBackupPrivateKey,
|
||||
|
||||
@@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {TestClient} from '../../../TestClient';
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import { TestClient } from '../../../TestClient';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import nodeCrypto from "crypto";
|
||||
import {logger} from '../../../../src/logger';
|
||||
import { logger } from '../../../../src/logger';
|
||||
|
||||
export async function makeTestClients(userInfos, options) {
|
||||
const clients = [];
|
||||
@@ -30,13 +30,13 @@ export async function makeTestClients(userInfos, options) {
|
||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||
if (deviceId in clientMap[userId]) {
|
||||
const event = new MatrixEvent({
|
||||
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
|
||||
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
|
||||
type: type,
|
||||
content: msg,
|
||||
});
|
||||
const client = clientMap[userId][deviceId];
|
||||
const decryptionPromise = event.isEncrypted() ?
|
||||
event.attemptDecryption(client._crypto) :
|
||||
event.attemptDecryption(client.crypto) :
|
||||
Promise.resolve();
|
||||
|
||||
decryptionPromise.then(
|
||||
@@ -49,9 +49,9 @@ export async function makeTestClients(userInfos, options) {
|
||||
};
|
||||
const sendEvent = function(room, type, content) {
|
||||
// make up a unique ID as the event ID
|
||||
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this
|
||||
const eventId = "$" + this.makeTxnId(); // eslint-disable-line @babel/no-invalid-this
|
||||
const rawEvent = {
|
||||
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
|
||||
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
|
||||
type: type,
|
||||
content: content,
|
||||
room_id: room,
|
||||
@@ -61,13 +61,13 @@ export async function makeTestClients(userInfos, options) {
|
||||
const event = new MatrixEvent(rawEvent);
|
||||
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
|
||||
unsigned: {
|
||||
transaction_id: this.makeTxnId(), // eslint-disable-line babel/no-invalid-this
|
||||
transaction_id: this.makeTxnId(), // eslint-disable-line @babel/no-invalid-this
|
||||
},
|
||||
}));
|
||||
|
||||
setImmediate(() => {
|
||||
for (const tc of clients) {
|
||||
if (tc.client === this) { // eslint-disable-line babel/no-invalid-this
|
||||
if (tc.client === this) { // eslint-disable-line @babel/no-invalid-this
|
||||
logger.log("sending remote echo!!");
|
||||
tc.client.emit("Room.timeline", remoteEcho);
|
||||
} else {
|
||||
@@ -76,7 +76,7 @@ export async function makeTestClients(userInfos, options) {
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.resolve({event_id: eventId});
|
||||
return Promise.resolve({ event_id: eventId });
|
||||
};
|
||||
|
||||
for (const userInfo of userInfos) {
|
||||
|
||||
@@ -13,13 +13,13 @@ 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 {VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE} from
|
||||
import { VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE } from
|
||||
"../../../../src/crypto/verification/request/VerificationRequest";
|
||||
import {InRoomChannel} from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import {ToDeviceChannel} from
|
||||
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import { ToDeviceChannel } from
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import {setupWebcrypto, teardownWebcrypto} from "./util";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { setupWebcrypto, teardownWebcrypto } from "./util";
|
||||
|
||||
function makeMockClient(userId, deviceId) {
|
||||
let counter = 1;
|
||||
@@ -40,7 +40,7 @@ function makeMockClient(userId, deviceId) {
|
||||
content,
|
||||
origin_server_ts: Date.now(),
|
||||
}));
|
||||
return Promise.resolve({event_id: eventId});
|
||||
return Promise.resolve({ event_id: eventId });
|
||||
},
|
||||
|
||||
sendToDevice(type, msgMap) {
|
||||
@@ -48,7 +48,7 @@ function makeMockClient(userId, deviceId) {
|
||||
const deviceMap = msgMap[userId];
|
||||
for (const deviceId of Object.keys(deviceMap)) {
|
||||
const content = deviceMap[deviceId];
|
||||
const event = new MatrixEvent({content, type});
|
||||
const event = new MatrixEvent({ content, type });
|
||||
deviceEvents[userId] = deviceEvents[userId] || {};
|
||||
deviceEvents[userId][deviceId] = deviceEvents[userId][deviceId] || [];
|
||||
deviceEvents[userId][deviceId].push(event);
|
||||
@@ -90,7 +90,7 @@ class MockVerifier {
|
||||
if (this._startEvent) {
|
||||
await this._channel.send(DONE_TYPE, {});
|
||||
} else {
|
||||
await this._channel.send(START_TYPE, {method: MOCK_METHOD});
|
||||
await this._channel.send(START_TYPE, { method: MOCK_METHOD });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ describe("verification request unit tests", function() {
|
||||
new ToDeviceChannel(bob1, bob1.getUserId(), ["device1", "device2"],
|
||||
ToDeviceChannel.makeTransactionId(), "device2"),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), bob1);
|
||||
const to = {userId: "@bob:matrix.tld", deviceId: "device2"};
|
||||
const to = { userId: "@bob:matrix.tld", deviceId: "device2" };
|
||||
const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to);
|
||||
expect(verifier).toBeInstanceOf(MockVerifier);
|
||||
await verifier.start();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import {RoomState} from "../../src/models/room-state";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { RoomState } from "../../src/models/room-state";
|
||||
|
||||
function mockRoomStates(timeline) {
|
||||
timeline._startState = utils.mock(RoomState, "startState");
|
||||
@@ -15,7 +15,7 @@ describe("EventTimeline", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
// XXX: this is a horrid hack; should use sinon or something instead to mock
|
||||
const timelineSet = { room: { roomId: roomId }};
|
||||
const timelineSet = { room: { roomId: roomId } };
|
||||
timelineSet.room.getUnfilteredTimelineSet = function() {
|
||||
return timelineSet;
|
||||
};
|
||||
@@ -94,7 +94,6 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("neighbouringTimelines", function() {
|
||||
it("neighbouring timelines should start null", function() {
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(null);
|
||||
@@ -102,8 +101,8 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
it("setNeighbouringTimeline should set neighbour", function() {
|
||||
const prev = {a: "a"};
|
||||
const next = {b: "b"};
|
||||
const prev = { a: "a" };
|
||||
const next = { b: "b" };
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
|
||||
@@ -111,8 +110,8 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
it("setNeighbouringTimeline should throw if called twice", function() {
|
||||
const prev = {a: "a"};
|
||||
const next = {b: "b"};
|
||||
const prev = { a: "a" };
|
||||
const next = { b: "b" };
|
||||
expect(function() {
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
}).not.toThrow();
|
||||
@@ -278,7 +277,6 @@ describe("EventTimeline", function() {
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for old events", function() {
|
||||
const events = [
|
||||
|
||||
@@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from "../../src/logger";
|
||||
import {MatrixEvent} from "../../src/models/event";
|
||||
import { logger } from "../../src/logger";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
describe(".attemptDecryption", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {FilterComponent} from "../../src/filter-component";
|
||||
import {mkEvent} from '../test-utils';
|
||||
import { FilterComponent } from "../../src/filter-component";
|
||||
import { mkEvent } from '../test-utils';
|
||||
|
||||
describe("Filter Component", function() {
|
||||
describe("types", function() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Filter} from "../../src/filter";
|
||||
import { Filter } from "../../src/filter";
|
||||
|
||||
describe("Filter", function() {
|
||||
const filterId = "f1lt3ring15g00d4ursoul";
|
||||
|
||||
@@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from "../../src/logger";
|
||||
import {InteractiveAuth} from "../../src/interactive-auth";
|
||||
import {MatrixError} from "../../src/http-api";
|
||||
import { logger } from "../../src/logger";
|
||||
import { InteractiveAuth } from "../../src/interactive-auth";
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
|
||||
// Trivial client object to test interactive auth
|
||||
// (we do not need TestClient here)
|
||||
@@ -63,7 +63,7 @@ describe("InteractiveAuth", function() {
|
||||
});
|
||||
|
||||
// .. which should trigger a call here
|
||||
const requestRes = {"a": "b"};
|
||||
const requestRes = { "a": "b" };
|
||||
doRequest.mockImplementation(function(authData) {
|
||||
logger.log('cccc');
|
||||
expect(authData).toEqual({
|
||||
@@ -112,7 +112,7 @@ describe("InteractiveAuth", function() {
|
||||
});
|
||||
|
||||
// .. which should be followed by a call to stateUpdated
|
||||
const requestRes = {"a": "b"};
|
||||
const requestRes = { "a": "b" };
|
||||
stateUpdated.mockImplementation(function(stage) {
|
||||
expect(stage).toEqual("logintype");
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {TestClient} from '../TestClient';
|
||||
import { TestClient } from '../TestClient';
|
||||
|
||||
describe('Login request', function() {
|
||||
let client;
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import {logger} from "../../src/logger";
|
||||
import {MatrixClient} from "../../src/client";
|
||||
import {Filter} from "../../src/filter";
|
||||
import { logger } from "../../src/logger";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { Filter } from "../../src/filter";
|
||||
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
|
||||
import {
|
||||
EventType,
|
||||
RoomCreateTypeField,
|
||||
RoomType,
|
||||
UNSTABLE_MSC3088_ENABLED,
|
||||
UNSTABLE_MSC3088_PURPOSE,
|
||||
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
||||
} from "../../src/@types/event";
|
||||
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Preset } from "../../src/@types/partials";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -144,12 +156,12 @@ describe("MatrixClient", function() {
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
});
|
||||
// FIXME: We shouldn't be yanking _http like this.
|
||||
client._http = [
|
||||
// FIXME: We shouldn't be yanking http like this.
|
||||
client.http = [
|
||||
"authedRequest", "getContentUri", "request", "uploadContent",
|
||||
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
||||
client._http.authedRequest.mockImplementation(httpReq);
|
||||
client._http.request.mockImplementation(httpReq);
|
||||
client.http.authedRequest.mockImplementation(httpReq);
|
||||
client.http.request.mockImplementation(httpReq);
|
||||
|
||||
// set reasonable working defaults
|
||||
acceptKeepalives = true;
|
||||
@@ -166,11 +178,165 @@ describe("MatrixClient", function() {
|
||||
// means they may call /events and then fail an expect() which will fail
|
||||
// a DIFFERENT test (pollution between tests!) - we return unresolved
|
||||
// promises to stop the client from continuing to run.
|
||||
client._http.authedRequest.mockImplementation(function() {
|
||||
client.http.authedRequest.mockImplementation(function() {
|
||||
return new Promise(() => {});
|
||||
});
|
||||
});
|
||||
|
||||
it("should create (unstable) file trees", async () => {
|
||||
const userId = "@test:example.org";
|
||||
const roomId = "!room:example.org";
|
||||
const roomName = "Test Tree";
|
||||
const mockRoom = {};
|
||||
const fn = jest.fn().mockImplementation((opts) => {
|
||||
expect(opts).toMatchObject({
|
||||
name: roomName,
|
||||
preset: Preset.PrivateChat,
|
||||
power_level_content_override: {
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users: {
|
||||
[userId]: 100,
|
||||
},
|
||||
},
|
||||
creation_content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
initial_state: [
|
||||
{
|
||||
// We use `unstable` to ensure that the code is actually using the right identifier
|
||||
type: UNSTABLE_MSC3088_PURPOSE.unstable,
|
||||
state_key: UNSTABLE_MSC3089_TREE_SUBTYPE.unstable,
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: EventType.RoomEncryption,
|
||||
state_key: "",
|
||||
content: {
|
||||
algorithm: MEGOLM_ALGORITHM,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
return { room_id: roomId };
|
||||
});
|
||||
client.getUserId = () => userId;
|
||||
client.createRoom = fn;
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
const tree = await client.unstableCreateFileTree(roomName);
|
||||
expect(tree).toBeDefined();
|
||||
expect(tree.roomId).toEqual(roomId);
|
||||
expect(tree.room).toBe(mockRoom);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should get (unstable) file trees with valid state", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
});
|
||||
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
|
||||
// We use `unstable` to ensure that the code is actually using the right identifier
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||
expect(tree).toBeDefined();
|
||||
expect(tree.roomId).toEqual(roomId);
|
||||
expect(tree.room).toBe(mockRoom);
|
||||
});
|
||||
|
||||
it("should not get (unstable) file trees with invalid create contents", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: "org.example.not_space",
|
||||
},
|
||||
});
|
||||
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
|
||||
// We use `unstable` to ensure that the code is actually using the right identifier
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||
expect(tree).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
});
|
||||
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||
expect(tree).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not POST /filter if a matching filter already exists", async function() {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
@@ -178,7 +344,7 @@ describe("MatrixClient", function() {
|
||||
const filterId = "ehfewf";
|
||||
store.getFilterIdByName.mockReturnValue(filterId);
|
||||
const filter = new Filter(0, filterId);
|
||||
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
|
||||
filter.setDefinition({ "room": { "timeline": { "limit": 8 } } });
|
||||
store.getFilter.mockReturnValue(filter);
|
||||
const syncPromise = new Promise((resolve, reject) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
@@ -521,4 +687,19 @@ describe("MatrixClient", function() {
|
||||
xit("should be able to peek into a room using peekInRoom", function(done) {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPresence", function() {
|
||||
it("should send a presence HTTP GET", function() {
|
||||
httpLookups = [{
|
||||
method: "GET",
|
||||
path: `/presence/${encodeURIComponent(userId)}/status`,
|
||||
data: {
|
||||
"presence": "unavailable",
|
||||
"last_active_ago": 420845,
|
||||
},
|
||||
}];
|
||||
client.getPresence(userId);
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
Copyright 2021 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 { MatrixClient } from "../../../src";
|
||||
import { Room } from "../../../src/models/room";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import { UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
|
||||
import { EventTimelineSet } from "../../../src/models/event-timeline-set";
|
||||
import { EventTimeline } from "../../../src/models/event-timeline";
|
||||
import { MSC3089Branch } from "../../../src/models/MSC3089Branch";
|
||||
|
||||
describe("MSC3089Branch", () => {
|
||||
let client: MatrixClient;
|
||||
// @ts-ignore - TS doesn't know that this is a type
|
||||
let indexEvent: MatrixEvent;
|
||||
let branch: MSC3089Branch;
|
||||
|
||||
const branchRoomId = "!room:example.org";
|
||||
const fileEventId = "$file";
|
||||
|
||||
const staticTimelineSets = {} as EventTimelineSet;
|
||||
const staticRoom = {
|
||||
getUnfilteredTimelineSet: () => staticTimelineSets,
|
||||
} as any as Room; // partial
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Use utility functions to create test rooms and clients
|
||||
client = <MatrixClient>{
|
||||
getRoom: (roomId: string) => {
|
||||
if (roomId === branchRoomId) {
|
||||
return staticRoom;
|
||||
} else {
|
||||
throw new Error("Unexpected fetch for unknown room");
|
||||
}
|
||||
},
|
||||
};
|
||||
indexEvent = {
|
||||
getRoomId: () => branchRoomId,
|
||||
getStateKey: () => fileEventId,
|
||||
};
|
||||
branch = new MSC3089Branch(client, indexEvent);
|
||||
});
|
||||
|
||||
it('should know the file event ID', () => {
|
||||
expect(branch.id).toEqual(fileEventId);
|
||||
});
|
||||
|
||||
it('should know if the file is active or not', () => {
|
||||
indexEvent.getContent = () => ({});
|
||||
expect(branch.isActive).toBe(false);
|
||||
indexEvent.getContent = () => ({ active: false });
|
||||
expect(branch.isActive).toBe(false);
|
||||
indexEvent.getContent = () => ({ active: true });
|
||||
expect(branch.isActive).toBe(true);
|
||||
indexEvent.getContent = () => ({ active: "true" }); // invalid boolean, inactive
|
||||
expect(branch.isActive).toBe(false);
|
||||
});
|
||||
|
||||
it('should be able to delete the file', async () => {
|
||||
const stateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
|
||||
expect(content).toMatchObject({});
|
||||
expect(content['active']).toBeUndefined();
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.sendStateEvent = stateFn;
|
||||
|
||||
const redactFn = jest.fn().mockImplementation((roomId: string, eventId: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventId).toEqual(fileEventId);
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.redactEvent = redactFn;
|
||||
|
||||
await branch.delete();
|
||||
|
||||
expect(stateFn).toHaveBeenCalledTimes(1);
|
||||
expect(redactFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should know its name', async () => {
|
||||
const name = "My File.txt";
|
||||
indexEvent.getContent = () => ({ active: true, name: name });
|
||||
|
||||
const res = branch.getName();
|
||||
|
||||
expect(res).toEqual(name);
|
||||
});
|
||||
|
||||
it('should be able to change its name', async () => {
|
||||
const name = "My File.txt";
|
||||
indexEvent.getContent = () => ({ active: true, retained: true });
|
||||
const stateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
|
||||
expect(content).toMatchObject({
|
||||
retained: true, // canary for copying state
|
||||
active: true,
|
||||
name: name,
|
||||
});
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.sendStateEvent = stateFn;
|
||||
|
||||
await branch.setName(name);
|
||||
|
||||
expect(stateFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should be able to return event information', async () => {
|
||||
const mxcLatter = "example.org/file";
|
||||
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
|
||||
const eventsArr = [
|
||||
{ getId: () => "$not-file", getContent: () => ({}) },
|
||||
{ getId: () => fileEventId, getContent: () => ({ file: fileContent }) },
|
||||
];
|
||||
client.getEventTimeline = () => Promise.resolve({
|
||||
getEvents: () => eventsArr,
|
||||
}) as any as Promise<EventTimeline>; // partial
|
||||
client.mxcUrlToHttp = (mxc: string) => {
|
||||
expect(mxc).toEqual("mxc://" + mxcLatter);
|
||||
return `https://example.org/_matrix/media/v1/download/${mxcLatter}`;
|
||||
};
|
||||
client.decryptEventIfNeeded = () => Promise.resolve();
|
||||
|
||||
const res = await branch.getFileInfo();
|
||||
expect(res).toBeDefined();
|
||||
expect(res).toMatchObject({
|
||||
info: fileContent,
|
||||
// Escape regex from MDN guides: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
||||
httpUrl: expect.stringMatching(`.+${mxcLatter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,857 @@
|
||||
/*
|
||||
Copyright 2021 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 { MatrixClient } from "../../../src";
|
||||
import { Room } from "../../../src/models/room";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import { EventType, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../../../src/@types/event";
|
||||
import {
|
||||
DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
MSC3089TreeSpace,
|
||||
TreePermissions,
|
||||
} from "../../../src/models/MSC3089TreeSpace";
|
||||
import { DEFAULT_ALPHABET } from "../../../src/utils";
|
||||
import { MockBlob } from "../../MockBlob";
|
||||
|
||||
describe("MSC3089TreeSpace", () => {
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
let tree: MSC3089TreeSpace;
|
||||
const roomId = "!tree:localhost";
|
||||
const targetUser = "@target:example.org";
|
||||
|
||||
let powerLevels;
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Use utility functions to create test rooms and clients
|
||||
client = <MatrixClient>{
|
||||
getRoom: (fetchRoomId: string) => {
|
||||
if (fetchRoomId === roomId) {
|
||||
return room;
|
||||
} else {
|
||||
throw new Error("Unexpected fetch for unknown room");
|
||||
}
|
||||
},
|
||||
};
|
||||
room = <Room>{
|
||||
currentState: {
|
||||
getStateEvents: (evType: EventType, stateKey: string) => {
|
||||
if (evType === EventType.RoomPowerLevels && stateKey === "") {
|
||||
return powerLevels;
|
||||
} else {
|
||||
throw new Error("Accessed unexpected state event type or key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
tree = new MSC3089TreeSpace(client, roomId);
|
||||
makePowerLevels(DEFAULT_TREE_POWER_LEVELS_TEMPLATE);
|
||||
});
|
||||
|
||||
function makePowerLevels(content: any) {
|
||||
powerLevels = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
state_key: "",
|
||||
sender: "@creator:localhost",
|
||||
event_id: "$powerlevels",
|
||||
room_id: roomId,
|
||||
content: content,
|
||||
});
|
||||
}
|
||||
|
||||
it('should populate the room reference', () => {
|
||||
expect(tree.room).toBe(room);
|
||||
});
|
||||
|
||||
it('should proxy the ID member to room ID', () => {
|
||||
expect(tree.id).toEqual(tree.roomId);
|
||||
expect(tree.id).toEqual(roomId);
|
||||
});
|
||||
|
||||
it('should support setting the name of the space', async () => {
|
||||
const newName = "NEW NAME";
|
||||
const fn = jest.fn()
|
||||
.mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect(stateRoomId).toEqual(roomId);
|
||||
expect(eventType).toEqual(EventType.RoomName);
|
||||
expect(stateKey).toEqual("");
|
||||
expect(content).toMatchObject({ name: newName });
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.sendStateEvent = fn;
|
||||
await tree.setName(newName);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should support inviting users to the space', async () => {
|
||||
const target = targetUser;
|
||||
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userId).toEqual(target);
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.invite = fn;
|
||||
await tree.invite(target);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) {
|
||||
makePowerLevels(pls);
|
||||
const fn = jest.fn()
|
||||
.mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect(stateRoomId).toEqual(roomId);
|
||||
expect(eventType).toEqual(EventType.RoomPowerLevels);
|
||||
expect(stateKey).toEqual("");
|
||||
expect(content).toMatchObject({
|
||||
...pls,
|
||||
users: {
|
||||
[targetUser]: expectedPl,
|
||||
},
|
||||
});
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.sendStateEvent = fn;
|
||||
await tree.setPermissions(targetUser, role);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
}
|
||||
|
||||
it('should support setting Viewer permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users_default: 1024,
|
||||
}, TreePermissions.Viewer, 1024);
|
||||
});
|
||||
|
||||
it('should support setting Editor permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
events_default: 1024,
|
||||
}, TreePermissions.Editor, 1024);
|
||||
});
|
||||
|
||||
it('should support setting Owner permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1024,
|
||||
},
|
||||
}, TreePermissions.Owner, 1024);
|
||||
});
|
||||
|
||||
it('should support demoting permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users_default: 1024,
|
||||
users: {
|
||||
[targetUser]: 2222,
|
||||
},
|
||||
}, TreePermissions.Viewer, 1024);
|
||||
});
|
||||
|
||||
it('should support promoting permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
events_default: 1024,
|
||||
users: {
|
||||
[targetUser]: 5,
|
||||
},
|
||||
}, TreePermissions.Editor, 1024);
|
||||
});
|
||||
|
||||
it('should support defaults: Viewer', () => {
|
||||
return evaluatePowerLevels({}, TreePermissions.Viewer, 0);
|
||||
});
|
||||
|
||||
it('should support defaults: Editor', () => {
|
||||
return evaluatePowerLevels({}, TreePermissions.Editor, 50);
|
||||
});
|
||||
|
||||
it('should support defaults: Owner', () => {
|
||||
return evaluatePowerLevels({}, TreePermissions.Owner, 100);
|
||||
});
|
||||
|
||||
it('should create subdirectories', async () => {
|
||||
const subspaceName = "subdirectory";
|
||||
const subspaceId = "!subspace:localhost";
|
||||
const domain = "domain.example.com";
|
||||
client.getRoom = (roomId: string) => {
|
||||
if (roomId === tree.roomId) {
|
||||
return tree.room;
|
||||
} else if (roomId === subspaceId) {
|
||||
return {} as Room; // we don't need anything important off of this
|
||||
} else {
|
||||
throw new Error("Unexpected getRoom call");
|
||||
}
|
||||
};
|
||||
client.getDomain = () => domain;
|
||||
const createFn = jest.fn().mockImplementation(async (name: string) => {
|
||||
expect(name).toEqual(subspaceName);
|
||||
return new MSC3089TreeSpace(client, subspaceId);
|
||||
});
|
||||
const sendStateFn = jest.fn()
|
||||
.mockImplementation(async (roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect([tree.roomId, subspaceId]).toContain(roomId);
|
||||
if (roomId === subspaceId) {
|
||||
expect(eventType).toEqual(EventType.SpaceParent);
|
||||
expect(stateKey).toEqual(tree.roomId);
|
||||
} else {
|
||||
expect(eventType).toEqual(EventType.SpaceChild);
|
||||
expect(stateKey).toEqual(subspaceId);
|
||||
}
|
||||
expect(content).toMatchObject({ via: [domain] });
|
||||
|
||||
// return value not used
|
||||
});
|
||||
client.unstableCreateFileTree = createFn;
|
||||
client.sendStateEvent = sendStateFn;
|
||||
|
||||
const directory = await tree.createDirectory(subspaceName);
|
||||
expect(directory).toBeDefined();
|
||||
expect(directory).not.toBeNull();
|
||||
expect(directory).not.toBe(tree);
|
||||
expect(directory.roomId).toEqual(subspaceId);
|
||||
expect(createFn).toHaveBeenCalledTimes(1);
|
||||
expect(sendStateFn).toHaveBeenCalledTimes(2);
|
||||
|
||||
const content = expect.objectContaining({ via: [domain] });
|
||||
expect(sendStateFn).toHaveBeenCalledWith(subspaceId, EventType.SpaceParent, content, tree.roomId);
|
||||
expect(sendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, content, subspaceId);
|
||||
});
|
||||
|
||||
it('should find subdirectories', () => {
|
||||
const firstChildRoom = "!one:example.org";
|
||||
const secondChildRoom = "!two:example.org";
|
||||
const thirdChildRoom = "!three:example.org"; // to ensure it doesn't end up in the subdirectories
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
||||
expect(eventType).toEqual(EventType.SpaceChild);
|
||||
expect(stateKey).toBeUndefined();
|
||||
return [
|
||||
// Partial implementations of Room
|
||||
{ getStateKey: () => firstChildRoom },
|
||||
{ getStateKey: () => secondChildRoom },
|
||||
{ getStateKey: () => thirdChildRoom },
|
||||
];
|
||||
},
|
||||
};
|
||||
client.getRoom = () => ({} as Room); // to appease the TreeSpace constructor
|
||||
|
||||
const getFn = jest.fn().mockImplementation((roomId: string) => {
|
||||
if (roomId === thirdChildRoom) {
|
||||
throw new Error("Mock not-a-space room case called (expected)");
|
||||
}
|
||||
expect([firstChildRoom, secondChildRoom]).toContain(roomId);
|
||||
return new MSC3089TreeSpace(client, roomId);
|
||||
});
|
||||
client.unstableGetFileTreeSpace = getFn;
|
||||
|
||||
const subdirectories = tree.getDirectories();
|
||||
expect(subdirectories).toBeDefined();
|
||||
expect(subdirectories.length).toBe(2);
|
||||
expect(subdirectories[0].roomId).toBe(firstChildRoom);
|
||||
expect(subdirectories[1].roomId).toBe(secondChildRoom);
|
||||
expect(getFn).toHaveBeenCalledTimes(3);
|
||||
expect(getFn).toHaveBeenCalledWith(firstChildRoom);
|
||||
expect(getFn).toHaveBeenCalledWith(secondChildRoom);
|
||||
expect(getFn).toHaveBeenCalledWith(thirdChildRoom); // check to make sure it tried
|
||||
});
|
||||
|
||||
it('should find specific directories', () => {
|
||||
client.getRoom = () => ({} as Room); // to appease the TreeSpace constructor
|
||||
|
||||
// Only mocking used API
|
||||
const firstSubdirectory = { roomId: "!first:example.org" } as any as MSC3089TreeSpace;
|
||||
const searchedSubdirectory = { roomId: "!find_me:example.org" } as any as MSC3089TreeSpace;
|
||||
const thirdSubdirectory = { roomId: "!third:example.org" } as any as MSC3089TreeSpace;
|
||||
tree.getDirectories = () => [firstSubdirectory, searchedSubdirectory, thirdSubdirectory];
|
||||
|
||||
let result = tree.getDirectory(searchedSubdirectory.roomId);
|
||||
expect(result).toBe(searchedSubdirectory);
|
||||
|
||||
result = tree.getDirectory("not a subdirectory");
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should be able to delete itself', async () => {
|
||||
const delete1 = jest.fn().mockImplementation(() => Promise.resolve());
|
||||
const subdir1 = { delete: delete1 } as any as MSC3089TreeSpace; // mock tested bits
|
||||
|
||||
const delete2 = jest.fn().mockImplementation(() => Promise.resolve());
|
||||
const subdir2 = { delete: delete2 } as any as MSC3089TreeSpace; // mock tested bits
|
||||
|
||||
const joinMemberId = "@join:example.org";
|
||||
const knockMemberId = "@knock:example.org";
|
||||
const inviteMemberId = "@invite:example.org";
|
||||
const leaveMemberId = "@leave:example.org";
|
||||
const banMemberId = "@ban:example.org";
|
||||
const selfUserId = "@self:example.org";
|
||||
|
||||
tree.getDirectories = () => [subdir1, subdir2];
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
||||
expect(eventType).toEqual(EventType.RoomMember);
|
||||
expect(stateKey).toBeUndefined();
|
||||
return [
|
||||
// Partial implementations
|
||||
{ getContent: () => ({ membership: "join" }), getStateKey: () => joinMemberId },
|
||||
{ getContent: () => ({ membership: "knock" }), getStateKey: () => knockMemberId },
|
||||
{ getContent: () => ({ membership: "invite" }), getStateKey: () => inviteMemberId },
|
||||
{ getContent: () => ({ membership: "leave" }), getStateKey: () => leaveMemberId },
|
||||
{ getContent: () => ({ membership: "ban" }), getStateKey: () => banMemberId },
|
||||
|
||||
// ensure we don't kick ourselves
|
||||
{ getContent: () => ({ membership: "join" }), getStateKey: () => selfUserId },
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
// These two functions are tested by input expectations, so no expectations in the function bodies
|
||||
const kickFn = jest.fn().mockImplementation((userId) => Promise.resolve());
|
||||
const leaveFn = jest.fn().mockImplementation(() => Promise.resolve());
|
||||
client.kick = kickFn;
|
||||
client.leave = leaveFn;
|
||||
client.getUserId = () => selfUserId;
|
||||
|
||||
await tree.delete();
|
||||
|
||||
expect(delete1).toHaveBeenCalledTimes(1);
|
||||
expect(delete2).toHaveBeenCalledTimes(1);
|
||||
expect(kickFn).toHaveBeenCalledTimes(3);
|
||||
expect(kickFn).toHaveBeenCalledWith(tree.roomId, joinMemberId, expect.any(String));
|
||||
expect(kickFn).toHaveBeenCalledWith(tree.roomId, knockMemberId, expect.any(String));
|
||||
expect(kickFn).toHaveBeenCalledWith(tree.roomId, inviteMemberId, expect.any(String));
|
||||
expect(leaveFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('get and set order', () => {
|
||||
// Danger: these are partial implementations for testing purposes only
|
||||
|
||||
// @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important
|
||||
let childState: { [roomId: string]: MatrixEvent[] } = {};
|
||||
// @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important
|
||||
let parentState: MatrixEvent[] = [];
|
||||
let parentRoom: Room;
|
||||
let childTrees: MSC3089TreeSpace[];
|
||||
let rooms: { [roomId: string]: Room };
|
||||
let clientSendStateFn: jest.MockedFunction<typeof client.sendStateEvent>;
|
||||
const staticDomain = "static.example.org";
|
||||
|
||||
function addSubspace(roomId: string, createTs?: number, order?: string) {
|
||||
const content = {
|
||||
via: [staticDomain],
|
||||
};
|
||||
if (order) content['order'] = order;
|
||||
parentState.push({
|
||||
getType: () => EventType.SpaceChild,
|
||||
getStateKey: () => roomId,
|
||||
getContent: () => content,
|
||||
});
|
||||
childState[roomId] = [
|
||||
{
|
||||
getType: () => EventType.SpaceParent,
|
||||
getStateKey: () => tree.roomId,
|
||||
getContent: () => ({
|
||||
via: [staticDomain],
|
||||
}),
|
||||
},
|
||||
];
|
||||
if (createTs) {
|
||||
childState[roomId].push({
|
||||
getType: () => EventType.RoomCreate,
|
||||
getStateKey: () => "",
|
||||
getContent: () => ({}),
|
||||
getTs: () => createTs,
|
||||
});
|
||||
}
|
||||
rooms[roomId] = makeMockChildRoom(roomId);
|
||||
childTrees.push(new MSC3089TreeSpace(client, roomId));
|
||||
}
|
||||
|
||||
function expectOrder(childRoomId: string, order: number) {
|
||||
const child = childTrees.find(c => c.roomId === childRoomId);
|
||||
expect(child).toBeDefined();
|
||||
expect(child.getOrder()).toEqual(order);
|
||||
}
|
||||
|
||||
function makeMockChildRoom(roomId: string): Room {
|
||||
return {
|
||||
currentState: {
|
||||
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
||||
expect([EventType.SpaceParent, EventType.RoomCreate]).toContain(eventType);
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return childState[roomId].find(e => e.getType() === EventType.RoomCreate);
|
||||
} else {
|
||||
expect(stateKey).toBeUndefined();
|
||||
return childState[roomId].filter(e => e.getType() === eventType);
|
||||
}
|
||||
},
|
||||
},
|
||||
} as Room; // partial
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
childState = {};
|
||||
parentState = [];
|
||||
parentRoom = {
|
||||
...tree.room,
|
||||
roomId: tree.roomId,
|
||||
currentState: {
|
||||
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
||||
expect([
|
||||
EventType.SpaceChild,
|
||||
EventType.RoomCreate,
|
||||
EventType.SpaceParent,
|
||||
]).toContain(eventType);
|
||||
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return parentState.filter(e => e.getType() === EventType.RoomCreate)[0];
|
||||
} else {
|
||||
if (stateKey !== undefined) {
|
||||
expect(Object.keys(rooms)).toContain(stateKey);
|
||||
expect(stateKey).not.toEqual(tree.roomId);
|
||||
return parentState.find(e => e.getType() === eventType && e.getStateKey() === stateKey);
|
||||
} // else fine
|
||||
return parentState.filter(e => e.getType() === eventType);
|
||||
}
|
||||
},
|
||||
},
|
||||
} as Room;
|
||||
childTrees = [];
|
||||
rooms = {};
|
||||
rooms[tree.roomId] = parentRoom;
|
||||
(<any>tree).room = parentRoom; // override readonly
|
||||
client.getRoom = (r) => rooms[r];
|
||||
|
||||
clientSendStateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
expect(eventType).toEqual(EventType.SpaceChild);
|
||||
expect(content).toMatchObject(expect.objectContaining({
|
||||
via: expect.any(Array),
|
||||
order: expect.any(String),
|
||||
}));
|
||||
expect(Object.keys(rooms)).toContain(stateKey);
|
||||
expect(stateKey).not.toEqual(tree.roomId);
|
||||
|
||||
const stateEvent = parentState.find(e => e.getType() === eventType && e.getStateKey() === stateKey);
|
||||
expect(stateEvent).toBeDefined();
|
||||
stateEvent.getContent = () => content;
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.sendStateEvent = clientSendStateFn;
|
||||
});
|
||||
|
||||
it('should know when something is top level', () => {
|
||||
const a = "!a:example.org";
|
||||
addSubspace(a);
|
||||
|
||||
expect(tree.isTopLevel).toBe(true);
|
||||
expect(childTrees[0].isTopLevel).toBe(false); // a bit of a hack to get at this, but it's fine
|
||||
});
|
||||
|
||||
it('should return -1 for top level spaces', () => {
|
||||
// The tree is what we've defined as top level, so it should work
|
||||
expect(tree.getOrder()).toEqual(-1);
|
||||
});
|
||||
|
||||
it('should throw when setting an order at the top level space', async () => {
|
||||
try {
|
||||
// The tree is what we've defined as top level, so it should work
|
||||
await tree.setOrder(2);
|
||||
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual("Cannot set order of top level spaces currently");
|
||||
}
|
||||
});
|
||||
|
||||
it('should return a stable order for unordered children', () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(c, 3);
|
||||
addSubspace(b, 2);
|
||||
addSubspace(a, 1);
|
||||
|
||||
expectOrder(a, 0);
|
||||
expectOrder(b, 1);
|
||||
expectOrder(c, 2);
|
||||
});
|
||||
|
||||
it('should return a stable order for ordered children', () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(a, 1, "Z");
|
||||
addSubspace(b, 2, "Y");
|
||||
addSubspace(c, 3, "X");
|
||||
|
||||
expectOrder(c, 0);
|
||||
expectOrder(b, 1);
|
||||
expectOrder(a, 2);
|
||||
});
|
||||
|
||||
it('should return a stable order for partially ordered children', () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
const d = "!d:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(a, 1);
|
||||
addSubspace(b, 2);
|
||||
addSubspace(c, 3, "Y");
|
||||
addSubspace(d, 4, "X");
|
||||
|
||||
expectOrder(d, 0);
|
||||
expectOrder(c, 1);
|
||||
expectOrder(b, 3); // note order diff due to room ID comparison expectation
|
||||
expectOrder(a, 2);
|
||||
});
|
||||
|
||||
it('should return a stable order if the create event timestamps are the same', () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(c, 3);
|
||||
addSubspace(b, 3); // same as C
|
||||
addSubspace(a, 3); // same as C
|
||||
|
||||
expectOrder(a, 0);
|
||||
expectOrder(b, 1);
|
||||
expectOrder(c, 2);
|
||||
});
|
||||
|
||||
it('should return a stable order if there are no known create events', () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(c);
|
||||
addSubspace(b);
|
||||
addSubspace(a);
|
||||
|
||||
expectOrder(a, 0);
|
||||
expectOrder(b, 1);
|
||||
expectOrder(c, 2);
|
||||
});
|
||||
|
||||
// XXX: These tests rely on `getOrder()` re-calculating and not caching values.
|
||||
|
||||
it('should allow reordering within unordered children', async () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(c, 3);
|
||||
addSubspace(b, 2);
|
||||
addSubspace(a, 1);
|
||||
|
||||
// Order of this state is validated by other tests.
|
||||
|
||||
const treeA = childTrees.find(c => c.roomId === a);
|
||||
expect(treeA).toBeDefined();
|
||||
await treeA.setOrder(1);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(3);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
|
||||
// Because of how the reordering works (maintain stable ordering before moving), we end up calling this
|
||||
// function twice for the same room.
|
||||
order: DEFAULT_ALPHABET[0],
|
||||
}), a);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: DEFAULT_ALPHABET[1],
|
||||
}), b);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: DEFAULT_ALPHABET[2],
|
||||
}), a);
|
||||
expectOrder(a, 1);
|
||||
expectOrder(b, 0);
|
||||
expectOrder(c, 2);
|
||||
});
|
||||
|
||||
it('should allow reordering within ordered children', async () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(c, 3, "Z");
|
||||
addSubspace(b, 2, "X");
|
||||
addSubspace(a, 1, "V");
|
||||
|
||||
// Order of this state is validated by other tests.
|
||||
|
||||
const treeA = childTrees.find(c => c.roomId === a);
|
||||
expect(treeA).toBeDefined();
|
||||
await treeA.setOrder(1);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: 'Y',
|
||||
}), a);
|
||||
expectOrder(a, 1);
|
||||
expectOrder(b, 0);
|
||||
expectOrder(c, 2);
|
||||
});
|
||||
|
||||
it('should allow reordering within partially ordered children', async () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
const d = "!d:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(a, 1);
|
||||
addSubspace(b, 2);
|
||||
addSubspace(c, 3, "Y");
|
||||
addSubspace(d, 4, "W");
|
||||
|
||||
// Order of this state is validated by other tests.
|
||||
|
||||
const treeA = childTrees.find(c => c.roomId === a);
|
||||
expect(treeA).toBeDefined();
|
||||
await treeA.setOrder(2);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: 'Z',
|
||||
}), a);
|
||||
expectOrder(a, 2);
|
||||
expectOrder(b, 3);
|
||||
expectOrder(c, 1);
|
||||
expectOrder(d, 0);
|
||||
});
|
||||
|
||||
it('should support moving upwards', async () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
const d = "!d:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(d, 4, "Z");
|
||||
addSubspace(c, 3, "X");
|
||||
addSubspace(b, 2, "V");
|
||||
addSubspace(a, 1, "T");
|
||||
|
||||
// Order of this state is validated by other tests.
|
||||
|
||||
const treeB = childTrees.find(c => c.roomId === b);
|
||||
expect(treeB).toBeDefined();
|
||||
await treeB.setOrder(2);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: 'Y',
|
||||
}), b);
|
||||
expectOrder(a, 0);
|
||||
expectOrder(b, 2);
|
||||
expectOrder(c, 1);
|
||||
expectOrder(d, 3);
|
||||
});
|
||||
|
||||
it('should support moving downwards', async () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
const d = "!d:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(d, 4, "Z");
|
||||
addSubspace(c, 3, "X");
|
||||
addSubspace(b, 2, "V");
|
||||
addSubspace(a, 1, "T");
|
||||
|
||||
// Order of this state is validated by other tests.
|
||||
|
||||
const treeC = childTrees.find(ch => ch.roomId === c);
|
||||
expect(treeC).toBeDefined();
|
||||
await treeC.setOrder(1);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: 'U',
|
||||
}), c);
|
||||
expectOrder(a, 0);
|
||||
expectOrder(b, 2);
|
||||
expectOrder(c, 1);
|
||||
expectOrder(d, 3);
|
||||
});
|
||||
|
||||
it('should support moving over the partial ordering boundary', async () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
const d = "!d:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(d, 4);
|
||||
addSubspace(c, 3);
|
||||
addSubspace(b, 2, "V");
|
||||
addSubspace(a, 1, "T");
|
||||
|
||||
// Order of this state is validated by other tests.
|
||||
|
||||
const treeB = childTrees.find(ch => ch.roomId === b);
|
||||
expect(treeB).toBeDefined();
|
||||
await treeB.setOrder(2);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(2);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: 'W',
|
||||
}), c);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: 'X',
|
||||
}), b);
|
||||
expectOrder(a, 0);
|
||||
expectOrder(b, 2);
|
||||
expectOrder(c, 1);
|
||||
expectOrder(d, 3);
|
||||
});
|
||||
});
|
||||
|
||||
it('should upload files', async () => {
|
||||
const mxc = "mxc://example.org/file";
|
||||
const fileInfo = {
|
||||
mimetype: "text/plain",
|
||||
// other fields as required by encryption, but ignored here
|
||||
};
|
||||
const fileEventId = "$file";
|
||||
const fileName = "My File.txt";
|
||||
const fileContents = "This is a test file";
|
||||
|
||||
// Mock out Blob for the test environment
|
||||
(<any>global).Blob = MockBlob;
|
||||
|
||||
const uploadFn = jest.fn().mockImplementation((contents: Blob, opts: any) => {
|
||||
expect(contents).toBeInstanceOf(Blob);
|
||||
expect(contents.size).toEqual(fileContents.length);
|
||||
expect(opts).toMatchObject({
|
||||
includeFilename: false,
|
||||
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
|
||||
});
|
||||
return Promise.resolve(mxc);
|
||||
});
|
||||
client.uploadContent = uploadFn;
|
||||
|
||||
const sendMsgFn = jest.fn().mockImplementation((roomId: string, contents: any) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
expect(contents).toMatchObject({
|
||||
msgtype: MsgType.File,
|
||||
body: fileName,
|
||||
url: mxc,
|
||||
file: fileInfo,
|
||||
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
|
||||
});
|
||||
|
||||
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
|
||||
});
|
||||
client.sendMessage = sendMsgFn;
|
||||
|
||||
const sendStateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
expect(content).toMatchObject({
|
||||
active: true,
|
||||
name: fileName,
|
||||
});
|
||||
|
||||
return Promise.resolve(); // return value not used.
|
||||
});
|
||||
client.sendStateEvent = sendStateFn;
|
||||
|
||||
const buf = Uint8Array.from(Array.from(fileContents).map((_, i) => fileContents.charCodeAt(i)));
|
||||
|
||||
// We clone the file info just to make sure it doesn't get mutated for the test.
|
||||
await tree.createFile(fileName, buf, Object.assign({}, fileInfo));
|
||||
|
||||
expect(uploadFn).toHaveBeenCalledTimes(1);
|
||||
expect(sendMsgFn).toHaveBeenCalledTimes(1);
|
||||
expect(sendStateFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should support getting files', () => {
|
||||
const fileEventId = "$file";
|
||||
const fileEvent = { forTest: true }; // MatrixEvent mock
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: string, stateKey?: string) => {
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
return fileEvent;
|
||||
},
|
||||
};
|
||||
|
||||
const file = tree.getFile(fileEventId);
|
||||
expect(file).toBeDefined();
|
||||
expect(file.indexEvent).toBe(fileEvent);
|
||||
});
|
||||
|
||||
it('should return falsy for unknown files', () => {
|
||||
const fileEventId = "$file";
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: string, stateKey?: string) => {
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const file = tree.getFile(fileEventId);
|
||||
expect(file).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should list files', () => {
|
||||
const firstFile = { getContent: () => ({ active: true }) };
|
||||
const secondFile = { getContent: () => ({ active: false }) }; // deliberately inactive
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: string, stateKey?: string) => {
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toBeUndefined();
|
||||
return [firstFile, secondFile];
|
||||
},
|
||||
};
|
||||
|
||||
const files = tree.listFiles();
|
||||
expect(files).toBeDefined();
|
||||
expect(files.length).toEqual(1);
|
||||
expect(files[0].indexEvent).toBe(firstFile);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {PushProcessor} from "../../src/pushprocessor";
|
||||
import { PushProcessor } from "../../src/pushprocessor";
|
||||
|
||||
describe('NotificationService', function() {
|
||||
const testUserId = "@ali:matrix.org";
|
||||
|
||||
@@ -46,8 +46,8 @@ describe("realtime-callbacks", function() {
|
||||
it("should set 'this' to the global object", function() {
|
||||
let passed = false;
|
||||
const callback = function() {
|
||||
expect(this).toBe(global); // eslint-disable-line babel/no-invalid-this
|
||||
expect(this.console).toBeTruthy(); // eslint-disable-line babel/no-invalid-this
|
||||
expect(this).toBe(global); // eslint-disable-line @babel/no-invalid-this
|
||||
expect(this.console).toBeTruthy(); // eslint-disable-line @babel/no-invalid-this
|
||||
passed = true;
|
||||
};
|
||||
callbacks.setTimeout(callback);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
Copyright 2021 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 { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Relations } from "../../src/models/relations";
|
||||
|
||||
describe("Relations", function() {
|
||||
it("should deduplicate annotations", function() {
|
||||
const relations = new Relations("m.annotation", "m.reaction");
|
||||
|
||||
// Create an instance of an annotation
|
||||
const eventData = {
|
||||
"sender": "@bob:example.com",
|
||||
"type": "m.reaction",
|
||||
"event_id": "$cZ1biX33ENJqIm00ks0W_hgiO_6CHrsAc3ZQrnLeNTw",
|
||||
"room_id": "!pzVjCQSoQPpXQeHpmK:example.com",
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
|
||||
"key": "👍️",
|
||||
"rel_type": "m.annotation",
|
||||
},
|
||||
},
|
||||
};
|
||||
const eventA = new MatrixEvent(eventData);
|
||||
|
||||
// Add the event once and check results
|
||||
{
|
||||
relations.addEvent(eventA);
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey();
|
||||
expect(annotationsByKey.length).toEqual(1);
|
||||
const [key, events] = annotationsByKey[0];
|
||||
expect(key).toEqual("👍️");
|
||||
expect(events.size).toEqual(1);
|
||||
}
|
||||
|
||||
// Add the event again and expect the same
|
||||
{
|
||||
relations.addEvent(eventA);
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey();
|
||||
expect(annotationsByKey.length).toEqual(1);
|
||||
const [key, events] = annotationsByKey[0];
|
||||
expect(key).toEqual("👍️");
|
||||
expect(events.size).toEqual(1);
|
||||
}
|
||||
|
||||
// Create a fresh object with the same event content
|
||||
const eventB = new MatrixEvent(eventData);
|
||||
|
||||
// Add the event again and expect the same
|
||||
{
|
||||
relations.addEvent(eventB);
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey();
|
||||
expect(annotationsByKey.length).toEqual(1);
|
||||
const [key, events] = annotationsByKey[0];
|
||||
expect(key).toEqual("👍️");
|
||||
expect(events.size).toEqual(1);
|
||||
}
|
||||
});
|
||||
|
||||
it("should emit created regardless of ordering", async function() {
|
||||
const targetEvent = new MatrixEvent({
|
||||
"sender": "@bob:example.com",
|
||||
"type": "m.room.message",
|
||||
"event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
|
||||
"room_id": "!pzVjCQSoQPpXQeHpmK:example.com",
|
||||
"content": {},
|
||||
});
|
||||
const relationEvent = new MatrixEvent({
|
||||
"sender": "@bob:example.com",
|
||||
"type": "m.reaction",
|
||||
"event_id": "$cZ1biX33ENJqIm00ks0W_hgiO_6CHrsAc3ZQrnLeNTw",
|
||||
"room_id": "!pzVjCQSoQPpXQeHpmK:example.com",
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
|
||||
"key": "👍️",
|
||||
"rel_type": "m.annotation",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Stub the room
|
||||
const room = {
|
||||
getPendingEvent() { return null; },
|
||||
getUnfilteredTimelineSet() { return null; },
|
||||
};
|
||||
|
||||
// Add the target event first, then the relation event
|
||||
{
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once("Event.relationsCreated", resolve);
|
||||
});
|
||||
|
||||
const timelineSet = new EventTimelineSet(room, {
|
||||
unstableClientRelationAggregation: true,
|
||||
});
|
||||
timelineSet.addLiveEvent(targetEvent);
|
||||
timelineSet.addLiveEvent(relationEvent);
|
||||
|
||||
await relationsCreated;
|
||||
}
|
||||
|
||||
// Add the relation event first, then the target event
|
||||
{
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once("Event.relationsCreated", resolve);
|
||||
});
|
||||
|
||||
const timelineSet = new EventTimelineSet(room, {
|
||||
unstableClientRelationAggregation: true,
|
||||
});
|
||||
timelineSet.addLiveEvent(relationEvent);
|
||||
timelineSet.addLiveEvent(targetEvent);
|
||||
|
||||
await relationsCreated;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {RoomMember} from "../../src/models/room-member";
|
||||
import { RoomMember } from "../../src/models/room-member";
|
||||
|
||||
describe("RoomMember", function() {
|
||||
const roomId = "!foo:bar";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {RoomState} from "../../src/models/room-state";
|
||||
import {RoomMember} from "../../src/models/room-member";
|
||||
import { RoomState } from "../../src/models/room-state";
|
||||
import { RoomMember } from "../../src/models/room-member";
|
||||
|
||||
describe("RoomState", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -471,13 +471,13 @@ describe("RoomState", function() {
|
||||
|
||||
it("should update after adding joined member", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userA, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "join",
|
||||
user: userA, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(1);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userC, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "join",
|
||||
user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(2);
|
||||
});
|
||||
@@ -490,13 +490,13 @@ describe("RoomState", function() {
|
||||
|
||||
it("should update after adding invited member", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userA, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "invite",
|
||||
user: userA, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(1);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userC, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "invite",
|
||||
user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(2);
|
||||
});
|
||||
@@ -509,15 +509,15 @@ describe("RoomState", function() {
|
||||
|
||||
it("should, once used, override counting members from state", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userA, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "join",
|
||||
user: userA, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(1);
|
||||
state.setJoinedMemberCount(100);
|
||||
expect(state.getJoinedMemberCount()).toEqual(100);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userC, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "join",
|
||||
user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(100);
|
||||
});
|
||||
@@ -525,14 +525,14 @@ describe("RoomState", function() {
|
||||
it("should, once used, override counting members from state, " +
|
||||
"also after clone", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userA, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "join",
|
||||
user: userA, room: roomId }),
|
||||
]);
|
||||
state.setJoinedMemberCount(100);
|
||||
const copy = state.clone();
|
||||
copy.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userC, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "join",
|
||||
user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(100);
|
||||
});
|
||||
@@ -545,15 +545,15 @@ describe("RoomState", function() {
|
||||
|
||||
it("should, once used, override counting members from state", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userB, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "invite",
|
||||
user: userB, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(1);
|
||||
state.setInvitedMemberCount(100);
|
||||
expect(state.getInvitedMemberCount()).toEqual(100);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userC, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "invite",
|
||||
user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(100);
|
||||
});
|
||||
@@ -561,14 +561,14 @@ describe("RoomState", function() {
|
||||
it("should, once used, override counting members from state, " +
|
||||
"also after clone", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userB, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "invite",
|
||||
user: userB, room: roomId }),
|
||||
]);
|
||||
state.setInvitedMemberCount(100);
|
||||
const copy = state.clone();
|
||||
copy.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userC, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "invite",
|
||||
user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(100);
|
||||
});
|
||||
|
||||
+35
-29
@@ -1,8 +1,9 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {EventStatus, MatrixEvent} from "../../src/models/event";
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import {RoomState} from "../../src/models/room-state";
|
||||
import {Room} from "../../src/models/room";
|
||||
import { EventStatus, MatrixEvent } from "../../src/models/event";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { RoomState } from "../../src/models/room-state";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("Room", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -190,7 +191,7 @@ describe("Room", function() {
|
||||
const remoteEvent = utils.mkMessage({
|
||||
room: roomId, user: userA, event: true,
|
||||
});
|
||||
remoteEvent.event.unsigned = {transaction_id: "TXN_ID"};
|
||||
remoteEvent.event.unsigned = { transaction_id: "TXN_ID" };
|
||||
const remoteEventId = remoteEvent.getId();
|
||||
|
||||
let callCount = 0;
|
||||
@@ -374,7 +375,7 @@ describe("Room", function() {
|
||||
let events = null;
|
||||
|
||||
beforeEach(function() {
|
||||
room = new Room(roomId, null, null, {timelineSupport: timelineSupport});
|
||||
room = new Room(roomId, null, null, { timelineSupport: timelineSupport });
|
||||
// set events each time to avoid resusing Event objects (which
|
||||
// doesn't work because they get frozen)
|
||||
events = [
|
||||
@@ -456,7 +457,7 @@ describe("Room", function() {
|
||||
|
||||
describe("compareEventOrdering", function() {
|
||||
beforeEach(function() {
|
||||
room = new Room(roomId, null, null, {timelineSupport: true});
|
||||
room = new Room(roomId, null, null, { timelineSupport: true });
|
||||
});
|
||||
|
||||
const events = [
|
||||
@@ -712,7 +713,7 @@ describe("Room", function() {
|
||||
it("uses hero name from state", function() {
|
||||
const name = "Mr B";
|
||||
addMember(userA, "invite");
|
||||
addMember(userB, "join", {name});
|
||||
addMember(userB, "join", { name });
|
||||
room.setSummary({
|
||||
"m.heroes": [userB],
|
||||
});
|
||||
@@ -723,7 +724,7 @@ describe("Room", function() {
|
||||
|
||||
it("uses counts from summary", function() {
|
||||
const name = "Mr B";
|
||||
addMember(userB, "join", {name});
|
||||
addMember(userB, "join", { name });
|
||||
room.setSummary({
|
||||
"m.heroes": [userB],
|
||||
"m.joined_member_count": 50,
|
||||
@@ -736,8 +737,8 @@ describe("Room", function() {
|
||||
it("relies on heroes in case of absent counts", function() {
|
||||
const nameB = "Mr Bean";
|
||||
const nameC = "Mel C";
|
||||
addMember(userB, "join", {name: nameB});
|
||||
addMember(userC, "join", {name: nameC});
|
||||
addMember(userB, "join", { name: nameB });
|
||||
addMember(userC, "join", { name: nameC });
|
||||
room.setSummary({
|
||||
"m.heroes": [userB, userC],
|
||||
});
|
||||
@@ -747,7 +748,7 @@ describe("Room", function() {
|
||||
|
||||
it("uses only heroes", function() {
|
||||
const nameB = "Mr Bean";
|
||||
addMember(userB, "join", {name: nameB});
|
||||
addMember(userB, "join", { name: nameB });
|
||||
addMember(userC, "join");
|
||||
room.setSummary({
|
||||
"m.heroes": [userB],
|
||||
@@ -840,7 +841,7 @@ describe("Room", function() {
|
||||
it("should show the other user's name for private" +
|
||||
" (invite join_rules) rooms if you are invited to it.", function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA, "invite", {user: userB});
|
||||
addMember(userA, "invite", { user: userB });
|
||||
addMember(userB);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
@@ -915,8 +916,8 @@ describe("Room", function() {
|
||||
"available",
|
||||
function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userB, 'join', {name: "Alice"});
|
||||
addMember(userA, "invite", {user: userA});
|
||||
addMember(userB, 'join', { name: "Alice" });
|
||||
addMember(userA, "invite", { user: userA });
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name).toEqual("Alice");
|
||||
@@ -926,7 +927,7 @@ describe("Room", function() {
|
||||
function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userB);
|
||||
addMember(userA, "invite", {user: userA});
|
||||
addMember(userA, "invite", { user: userA });
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name).toEqual(userB);
|
||||
@@ -1176,7 +1177,10 @@ describe("Room", function() {
|
||||
describe("addPendingEvent", function() {
|
||||
it("should add pending events to the pendingEventList if " +
|
||||
"pendingEventOrdering == 'detached'", function() {
|
||||
const room = new Room(roomId, null, userA, {
|
||||
const client = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const room = new Room(roomId, client, userA, {
|
||||
pendingEventOrdering: "detached",
|
||||
});
|
||||
const eventA = utils.mkMessage({
|
||||
@@ -1226,7 +1230,10 @@ describe("Room", function() {
|
||||
|
||||
describe("updatePendingEvent", function() {
|
||||
it("should remove cancelled events from the pending list", function() {
|
||||
const room = new Room(roomId, null, userA, {
|
||||
const client = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const room = new Room(roomId, client, userA, {
|
||||
pendingEventOrdering: "detached",
|
||||
});
|
||||
const eventA = utils.mkMessage({
|
||||
@@ -1260,7 +1267,6 @@ describe("Room", function() {
|
||||
expect(callCount).toEqual(1);
|
||||
});
|
||||
|
||||
|
||||
it("should remove cancelled events from the timeline", function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
const eventA = utils.mkMessage({
|
||||
@@ -1308,13 +1314,13 @@ describe("Room", function() {
|
||||
isRoomEncrypted: function() {
|
||||
return false;
|
||||
},
|
||||
_http: {
|
||||
http: {
|
||||
serverResponse,
|
||||
authedRequest: function() {
|
||||
if (this.serverResponse instanceof Error) {
|
||||
return Promise.reject(this.serverResponse);
|
||||
} else {
|
||||
return Promise.resolve({chunk: this.serverResponse});
|
||||
return Promise.resolve({ chunk: this.serverResponse });
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -1344,7 +1350,7 @@ describe("Room", function() {
|
||||
|
||||
it("should load members from server on first call", async function() {
|
||||
const client = createClientMock([memberEvent]);
|
||||
const room = new Room(roomId, client, null, {lazyLoadMembers: true});
|
||||
const room = new Room(roomId, client, null, { lazyLoadMembers: true });
|
||||
await room.loadMembersIfNeeded();
|
||||
const memberA = room.getMember("@user_a:bar");
|
||||
expect(memberA.name).toEqual("User A");
|
||||
@@ -1359,7 +1365,7 @@ describe("Room", function() {
|
||||
room: roomId, event: true, name: "Ms A",
|
||||
});
|
||||
const client = createClientMock([memberEvent2], [memberEvent]);
|
||||
const room = new Room(roomId, client, null, {lazyLoadMembers: true});
|
||||
const room = new Room(roomId, client, null, { lazyLoadMembers: true });
|
||||
|
||||
await room.loadMembersIfNeeded();
|
||||
|
||||
@@ -1369,7 +1375,7 @@ describe("Room", function() {
|
||||
|
||||
it("should allow retry on error", async function() {
|
||||
const client = createClientMock(new Error("server says no"));
|
||||
const room = new Room(roomId, client, null, {lazyLoadMembers: true});
|
||||
const room = new Room(roomId, client, null, { lazyLoadMembers: true });
|
||||
let hasThrown = false;
|
||||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
@@ -1378,7 +1384,7 @@ describe("Room", function() {
|
||||
}
|
||||
expect(hasThrown).toEqual(true);
|
||||
|
||||
client._http.serverResponse = [memberEvent];
|
||||
client.http.serverResponse = [memberEvent];
|
||||
await room.loadMembersIfNeeded();
|
||||
const memberA = room.getMember("@user_a:bar");
|
||||
expect(memberA.name).toEqual("User A");
|
||||
@@ -1397,17 +1403,17 @@ describe("Room", function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
const events = [];
|
||||
room.on("Room.myMembership", (_room, membership, oldMembership) => {
|
||||
events.push({membership, oldMembership});
|
||||
events.push({ membership, oldMembership });
|
||||
});
|
||||
room.updateMyMembership("invite");
|
||||
expect(room.getMyMembership()).toEqual("invite");
|
||||
expect(events[0]).toEqual({membership: "invite", oldMembership: null});
|
||||
expect(events[0]).toEqual({ membership: "invite", oldMembership: null });
|
||||
events.splice(0); //clear
|
||||
room.updateMyMembership("invite");
|
||||
expect(events.length).toEqual(0);
|
||||
room.updateMyMembership("join");
|
||||
expect(room.getMyMembership()).toEqual("join");
|
||||
expect(events[0]).toEqual({membership: "join", oldMembership: "invite"});
|
||||
expect(events[0]).toEqual({ membership: "join", oldMembership: "invite" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1415,7 +1421,7 @@ describe("Room", function() {
|
||||
it("should return first hero id",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.setSummary({'m.heroes': [userB]});
|
||||
room.setSummary({ 'm.heroes': [userB] });
|
||||
expect(room.guessDMUserId()).toEqual(userB);
|
||||
});
|
||||
it("should return first member that isn't self",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// This file had a function whose name is all caps, which displeases eslint
|
||||
/* eslint new-cap: "off" */
|
||||
|
||||
import {defer} from '../../src/utils';
|
||||
import {MatrixError} from "../../src/http-api";
|
||||
import {MatrixScheduler} from "../../src/scheduler";
|
||||
import { defer } from '../../src/utils';
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import * as utils from "../test-utils";
|
||||
|
||||
jest.useFakeTimers();
|
||||
@@ -62,8 +62,8 @@ describe("MatrixScheduler", function() {
|
||||
scheduler.queueEvent(eventA),
|
||||
scheduler.queueEvent(eventB),
|
||||
]);
|
||||
deferB.resolve({b: true});
|
||||
deferA.resolve({a: true});
|
||||
deferB.resolve({ b: true });
|
||||
deferA.resolve({ a: true });
|
||||
const [a, b] = await abPromise;
|
||||
expect(a.a).toEqual(true);
|
||||
expect(b.b).toEqual(true);
|
||||
@@ -156,8 +156,8 @@ describe("MatrixScheduler", function() {
|
||||
// Expect to have processFn invoked for A&B.
|
||||
// Resolve A.
|
||||
// Expect to have processFn invoked for D.
|
||||
const eventC = utils.mkMessage({user: "@a:bar", room: roomId, event: true});
|
||||
const eventD = utils.mkMessage({user: "@b:bar", room: roomId, event: true});
|
||||
const eventC = utils.mkMessage({ user: "@a:bar", room: roomId, event: true });
|
||||
const eventD = utils.mkMessage({ user: "@b:bar", room: roomId, event: true });
|
||||
|
||||
const buckets = {};
|
||||
buckets[eventA.getId()] = "queue_A";
|
||||
@@ -241,7 +241,7 @@ describe("MatrixScheduler", function() {
|
||||
expect(queue).toEqual([eventA, eventB]);
|
||||
// modify the queue
|
||||
const eventC = utils.mkMessage(
|
||||
{user: "@a:bar", room: roomId, event: true},
|
||||
{ user: "@a:bar", room: roomId, event: true },
|
||||
);
|
||||
queue.push(eventC);
|
||||
const queueAgain = scheduler.getQueueForEvent(eventA);
|
||||
|
||||
@@ -15,7 +15,40 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {SyncAccumulator} from "../../src/sync-accumulator";
|
||||
import { SyncAccumulator } from "../../src/sync-accumulator";
|
||||
|
||||
// The event body & unsigned object get frozen to assert that they don't get altered
|
||||
// by the impl
|
||||
const RES_WITH_AGE = {
|
||||
next_batch: "abc",
|
||||
rooms: {
|
||||
invite: {},
|
||||
leave: {},
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
account_data: { events: [] },
|
||||
ephemeral: { events: [] },
|
||||
unread_notifications: {},
|
||||
timeline: {
|
||||
events: [
|
||||
Object.freeze({
|
||||
content: {
|
||||
body: "This thing is happening right now!",
|
||||
},
|
||||
origin_server_ts: 123456789,
|
||||
sender: "@alice:localhost",
|
||||
type: "m.room.message",
|
||||
unsigned: Object.freeze({
|
||||
age: 50,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
prev_batch: "something",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("SyncAccumulator", function() {
|
||||
let sa;
|
||||
@@ -368,6 +401,39 @@ describe("SyncAccumulator", function() {
|
||||
expect(summary["m.joined_member_count"]).toEqual(5);
|
||||
expect(summary["m.heroes"]).toEqual(["@bob:bar"]);
|
||||
});
|
||||
|
||||
it("should return correctly adjusted age attributes", () => {
|
||||
const delta = 1000;
|
||||
const startingTs = 1000;
|
||||
|
||||
const oldDateNow = Date.now;
|
||||
try {
|
||||
Date.now = jest.fn();
|
||||
Date.now.mockReturnValue(startingTs);
|
||||
|
||||
sa.accumulate(RES_WITH_AGE);
|
||||
|
||||
Date.now.mockReturnValue(startingTs + delta);
|
||||
|
||||
const output = sa.getJSON();
|
||||
expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned.age).toEqual(
|
||||
RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned.age + delta,
|
||||
);
|
||||
expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual(
|
||||
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
|
||||
);
|
||||
} finally {
|
||||
Date.now = oldDateNow;
|
||||
}
|
||||
});
|
||||
|
||||
it("should mangle age without adding extra keys", () => {
|
||||
sa.accumulate(RES_WITH_AGE);
|
||||
const output = sa.getJSON();
|
||||
expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual(
|
||||
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import {TimelineIndex, TimelineWindow} from "../../src/timeline-window";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { TimelineIndex, TimelineWindow } from "../../src/timeline-window";
|
||||
import * as utils from "../test-utils";
|
||||
|
||||
const ROOM_ID = "roomId";
|
||||
@@ -18,7 +18,7 @@ function createTimeline(numEvents, baseIndex) {
|
||||
}
|
||||
|
||||
// XXX: this is a horrid hack
|
||||
const timelineSet = { room: { roomId: ROOM_ID }};
|
||||
const timelineSet = { room: { roomId: ROOM_ID } };
|
||||
timelineSet.room.getUnfilteredTimelineSet = function() {
|
||||
return timelineSet;
|
||||
};
|
||||
@@ -46,7 +46,6 @@ function addEventsToTimeline(timeline, numEvents, atStart) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* create a pair of linked timelines
|
||||
*/
|
||||
@@ -58,7 +57,6 @@ function createLinkedTimelines() {
|
||||
return [tl1, tl2];
|
||||
}
|
||||
|
||||
|
||||
describe("TimelineIndex", function() {
|
||||
describe("minIndex", function() {
|
||||
it("should return the min index relative to BaseIndex", function() {
|
||||
@@ -133,7 +131,6 @@ describe("TimelineIndex", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("TimelineWindow", function() {
|
||||
/**
|
||||
* create a dummy eventTimelineSet and client, and a TimelineWindow
|
||||
@@ -142,7 +139,7 @@ describe("TimelineWindow", function() {
|
||||
let timelineSet;
|
||||
let client;
|
||||
function createWindow(timeline, opts) {
|
||||
timelineSet = {getTimelineForEvent: () => null};
|
||||
timelineSet = { getTimelineForEvent: () => null };
|
||||
client = {};
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineSet0).toBe(timelineSet);
|
||||
@@ -171,7 +168,7 @@ describe("TimelineWindow", function() {
|
||||
const timeline = createTimeline();
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
const timelineSet = {getTimelineForEvent: () => null};
|
||||
const timelineSet = { getTimelineForEvent: () => null };
|
||||
const client = {};
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineSet0).toBe(timelineSet);
|
||||
@@ -193,7 +190,7 @@ describe("TimelineWindow", function() {
|
||||
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
const timelineSet = {getTimelineForEvent: () => null};
|
||||
const timelineSet = { getTimelineForEvent: () => null };
|
||||
const client = {};
|
||||
|
||||
const timelineWindow = new TimelineWindow(client, timelineSet);
|
||||
@@ -266,7 +263,7 @@ describe("TimelineWindow", function() {
|
||||
it("should advance into next timeline", function() {
|
||||
const tls = createLinkedTimelines();
|
||||
const eventId = tls[0].getEvents()[1].getId();
|
||||
const timelineWindow = createWindow(tls[0], {windowLimit: 5});
|
||||
const timelineWindow = createWindow(tls[0], { windowLimit: 5 });
|
||||
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = tls[0].getEvents();
|
||||
@@ -311,7 +308,7 @@ describe("TimelineWindow", function() {
|
||||
it("should retreat into previous timeline", function() {
|
||||
const tls = createLinkedTimelines();
|
||||
const eventId = tls[1].getEvents()[1].getId();
|
||||
const timelineWindow = createWindow(tls[1], {windowLimit: 5});
|
||||
const timelineWindow = createWindow(tls[1], { windowLimit: 5 });
|
||||
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = tls[1].getEvents();
|
||||
@@ -357,7 +354,7 @@ describe("TimelineWindow", function() {
|
||||
const timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
|
||||
|
||||
const timelineWindow = createWindow(timeline, {windowLimit: 5});
|
||||
const timelineWindow = createWindow(timeline, { windowLimit: 5 });
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
client.paginateEventTimeline = function(timeline0, opts) {
|
||||
@@ -385,12 +382,11 @@ describe("TimelineWindow", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should make backward pagination requests", function() {
|
||||
const timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.BACKWARDS);
|
||||
|
||||
const timelineWindow = createWindow(timeline, {windowLimit: 5});
|
||||
const timelineWindow = createWindow(timeline, { windowLimit: 5 });
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
client.paginateEventTimeline = function(timeline0, opts) {
|
||||
@@ -422,7 +418,7 @@ describe("TimelineWindow", function() {
|
||||
const timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
|
||||
|
||||
const timelineWindow = createWindow(timeline, {windowLimit: 5});
|
||||
const timelineWindow = createWindow(timeline, { windowLimit: 5 });
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
let paginateCount = 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {User} from "../../src/models/user";
|
||||
import { User } from "../../src/models/user";
|
||||
import * as utils from "../test-utils";
|
||||
|
||||
describe("User", function() {
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
import * as utils from "../../src/utils";
|
||||
|
||||
describe("utils", function() {
|
||||
describe("encodeParams", function() {
|
||||
it("should url encode and concat with &s", function() {
|
||||
const params = {
|
||||
foo: "bar",
|
||||
baz: "beer@",
|
||||
};
|
||||
expect(utils.encodeParams(params)).toEqual(
|
||||
"foo=bar&baz=beer%40",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encodeUri", function() {
|
||||
it("should replace based on object keys and url encode", function() {
|
||||
const path = "foo/bar/%something/%here";
|
||||
const vals = {
|
||||
"%something": "baz",
|
||||
"%here": "beer@",
|
||||
};
|
||||
expect(utils.encodeUri(path, vals)).toEqual(
|
||||
"foo/bar/baz/beer%40",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("forEach", function() {
|
||||
it("should be invoked for each element", function() {
|
||||
const arr = [];
|
||||
utils.forEach([55, 66, 77], function(element) {
|
||||
arr.push(element);
|
||||
});
|
||||
expect(arr).toEqual([55, 66, 77]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findElement", function() {
|
||||
it("should find only 1 element if there is a match", function() {
|
||||
const matchFn = function() {
|
||||
return true;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
expect(utils.findElement(arr, matchFn)).toEqual(55);
|
||||
});
|
||||
it("should be able to find in reverse order", function() {
|
||||
const matchFn = function() {
|
||||
return true;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
expect(utils.findElement(arr, matchFn, true)).toEqual(77);
|
||||
});
|
||||
it("should find nothing if the function never returns true", function() {
|
||||
const matchFn = function() {
|
||||
return false;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
expect(utils.findElement(arr, matchFn)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeElement", function() {
|
||||
it("should remove only 1 element if there is a match", function() {
|
||||
const matchFn = function() {
|
||||
return true;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
utils.removeElement(arr, matchFn);
|
||||
expect(arr).toEqual([66, 77]);
|
||||
});
|
||||
it("should be able to remove in reverse order", function() {
|
||||
const matchFn = function() {
|
||||
return true;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
utils.removeElement(arr, matchFn, true);
|
||||
expect(arr).toEqual([55, 66]);
|
||||
});
|
||||
it("should remove nothing if the function never returns true", function() {
|
||||
const matchFn = function() {
|
||||
return false;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
utils.removeElement(arr, matchFn);
|
||||
expect(arr).toEqual(arr);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isFunction", function() {
|
||||
it("should return true for functions", function() {
|
||||
expect(utils.isFunction([])).toBe(false);
|
||||
expect(utils.isFunction([5, 3, 7])).toBe(false);
|
||||
expect(utils.isFunction()).toBe(false);
|
||||
expect(utils.isFunction(null)).toBe(false);
|
||||
expect(utils.isFunction({})).toBe(false);
|
||||
expect(utils.isFunction("foo")).toBe(false);
|
||||
expect(utils.isFunction(555)).toBe(false);
|
||||
|
||||
expect(utils.isFunction(function() {})).toBe(true);
|
||||
const s = { foo: function() {} };
|
||||
expect(utils.isFunction(s.foo)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isArray", function() {
|
||||
it("should return true for arrays", function() {
|
||||
expect(utils.isArray([])).toBe(true);
|
||||
expect(utils.isArray([5, 3, 7])).toBe(true);
|
||||
|
||||
expect(utils.isArray()).toBe(false);
|
||||
expect(utils.isArray(null)).toBe(false);
|
||||
expect(utils.isArray({})).toBe(false);
|
||||
expect(utils.isArray("foo")).toBe(false);
|
||||
expect(utils.isArray(555)).toBe(false);
|
||||
expect(utils.isArray(function() {})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkObjectHasKeys", function() {
|
||||
it("should throw for missing keys", function() {
|
||||
expect(function() {
|
||||
utils.checkObjectHasKeys({}, ["foo"]);
|
||||
}).toThrow();
|
||||
expect(function() {
|
||||
utils.checkObjectHasKeys({
|
||||
foo: "bar",
|
||||
}, ["foo"]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkObjectHasNoAdditionalKeys", function() {
|
||||
it("should throw for extra keys", function() {
|
||||
expect(function() {
|
||||
utils.checkObjectHasNoAdditionalKeys({
|
||||
foo: "bar",
|
||||
baz: 4,
|
||||
}, ["foo"]);
|
||||
}).toThrow();
|
||||
|
||||
expect(function() {
|
||||
utils.checkObjectHasNoAdditionalKeys({
|
||||
foo: "bar",
|
||||
}, ["foo"]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deepCompare", function() {
|
||||
const assert = {
|
||||
isTrue: function(x) {
|
||||
expect(x).toBe(true);
|
||||
},
|
||||
isFalse: function(x) {
|
||||
expect(x).toBe(false);
|
||||
},
|
||||
};
|
||||
|
||||
it("should handle primitives", function() {
|
||||
assert.isTrue(utils.deepCompare(null, null));
|
||||
assert.isFalse(utils.deepCompare(null, undefined));
|
||||
assert.isTrue(utils.deepCompare("hi", "hi"));
|
||||
assert.isTrue(utils.deepCompare(5, 5));
|
||||
assert.isFalse(utils.deepCompare(5, 10));
|
||||
});
|
||||
|
||||
it("should handle regexps", function() {
|
||||
assert.isTrue(utils.deepCompare(/abc/, /abc/));
|
||||
assert.isFalse(utils.deepCompare(/abc/, /123/));
|
||||
const r = /abc/;
|
||||
assert.isTrue(utils.deepCompare(r, r));
|
||||
});
|
||||
|
||||
it("should handle dates", function() {
|
||||
assert.isTrue(utils.deepCompare(new Date("2011-03-31"),
|
||||
new Date("2011-03-31")));
|
||||
assert.isFalse(utils.deepCompare(new Date("2011-03-31"),
|
||||
new Date("1970-01-01")));
|
||||
});
|
||||
|
||||
it("should handle arrays", function() {
|
||||
assert.isTrue(utils.deepCompare([], []));
|
||||
assert.isTrue(utils.deepCompare([1, 2], [1, 2]));
|
||||
assert.isFalse(utils.deepCompare([1, 2], [2, 1]));
|
||||
assert.isFalse(utils.deepCompare([1, 2], [1, 2, 3]));
|
||||
});
|
||||
|
||||
it("should handle simple objects", function() {
|
||||
assert.isTrue(utils.deepCompare({}, {}));
|
||||
assert.isTrue(utils.deepCompare({a: 1, b: 2}, {a: 1, b: 2}));
|
||||
assert.isTrue(utils.deepCompare({a: 1, b: 2}, {b: 2, a: 1}));
|
||||
assert.isFalse(utils.deepCompare({a: 1, b: 2}, {a: 1, b: 3}));
|
||||
|
||||
assert.isTrue(utils.deepCompare({1: {name: "mhc", age: 28},
|
||||
2: {name: "arb", age: 26}},
|
||||
{1: {name: "mhc", age: 28},
|
||||
2: {name: "arb", age: 26}}));
|
||||
|
||||
assert.isFalse(utils.deepCompare({1: {name: "mhc", age: 28},
|
||||
2: {name: "arb", age: 26}},
|
||||
{1: {name: "mhc", age: 28},
|
||||
2: {name: "arb", age: 27}}));
|
||||
|
||||
assert.isFalse(utils.deepCompare({}, null));
|
||||
assert.isFalse(utils.deepCompare({}, undefined));
|
||||
});
|
||||
|
||||
it("should handle functions", function() {
|
||||
// no two different function is equal really, they capture their
|
||||
// context variables so even if they have same toString(), they
|
||||
// won't have same functionality
|
||||
const func = function(x) {
|
||||
return true;
|
||||
};
|
||||
const func2 = function(x) {
|
||||
return true;
|
||||
};
|
||||
assert.isTrue(utils.deepCompare(func, func));
|
||||
assert.isFalse(utils.deepCompare(func, func2));
|
||||
assert.isTrue(utils.deepCompare({ a: { b: func } }, { a: { b: func } }));
|
||||
assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } }));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("extend", function() {
|
||||
const SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
|
||||
|
||||
it("should extend", function() {
|
||||
const target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
const merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
const sourceOrig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
|
||||
});
|
||||
|
||||
it("should ignore null", function() {
|
||||
const target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
const merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
const sourceOrig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, null, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
|
||||
});
|
||||
|
||||
it("should handle properties created with defineProperties", function() {
|
||||
const source = Object.defineProperties({}, {
|
||||
"enumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
"nonenumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const target = {};
|
||||
utils.extend(target, source);
|
||||
expect(target.enumerableProp).toBe(true);
|
||||
expect(target.nonenumerableProp).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,432 @@
|
||||
import * as utils from "../../src/utils";
|
||||
import {
|
||||
alphabetPad,
|
||||
averageBetweenStrings,
|
||||
baseToString,
|
||||
DEFAULT_ALPHABET,
|
||||
lexicographicCompare,
|
||||
nextString,
|
||||
prevString,
|
||||
stringToBase,
|
||||
} from "../../src/utils";
|
||||
import { logger } from "../../src/logger";
|
||||
|
||||
// TODO: Fix types throughout
|
||||
|
||||
describe("utils", function() {
|
||||
describe("encodeParams", function() {
|
||||
it("should url encode and concat with &s", function() {
|
||||
const params = {
|
||||
foo: "bar",
|
||||
baz: "beer@",
|
||||
};
|
||||
expect(utils.encodeParams(params)).toEqual(
|
||||
"foo=bar&baz=beer%40",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encodeUri", function() {
|
||||
it("should replace based on object keys and url encode", function() {
|
||||
const path = "foo/bar/%something/%here";
|
||||
const vals = {
|
||||
"%something": "baz",
|
||||
"%here": "beer@",
|
||||
};
|
||||
expect(utils.encodeUri(path, vals)).toEqual(
|
||||
"foo/bar/baz/beer%40",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeElement", function() {
|
||||
it("should remove only 1 element if there is a match", function() {
|
||||
const matchFn = function() {
|
||||
return true;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
utils.removeElement(arr, matchFn);
|
||||
expect(arr).toEqual([66, 77]);
|
||||
});
|
||||
it("should be able to remove in reverse order", function() {
|
||||
const matchFn = function() {
|
||||
return true;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
utils.removeElement(arr, matchFn, true);
|
||||
expect(arr).toEqual([55, 66]);
|
||||
});
|
||||
it("should remove nothing if the function never returns true", function() {
|
||||
const matchFn = function() {
|
||||
return false;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
utils.removeElement(arr, matchFn);
|
||||
expect(arr).toEqual(arr);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isFunction", function() {
|
||||
it("should return true for functions", function() {
|
||||
expect(utils.isFunction([])).toBe(false);
|
||||
expect(utils.isFunction([5, 3, 7])).toBe(false);
|
||||
expect(utils.isFunction(undefined)).toBe(false);
|
||||
expect(utils.isFunction(null)).toBe(false);
|
||||
expect(utils.isFunction({})).toBe(false);
|
||||
expect(utils.isFunction("foo")).toBe(false);
|
||||
expect(utils.isFunction(555)).toBe(false);
|
||||
|
||||
expect(utils.isFunction(function() {})).toBe(true);
|
||||
const s = { foo: function() {} };
|
||||
expect(utils.isFunction(s.foo)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkObjectHasKeys", function() {
|
||||
it("should throw for missing keys", function() {
|
||||
expect(function() {
|
||||
utils.checkObjectHasKeys({}, ["foo"]);
|
||||
}).toThrow();
|
||||
expect(function() {
|
||||
utils.checkObjectHasKeys({
|
||||
foo: "bar",
|
||||
}, ["foo"]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkObjectHasNoAdditionalKeys", function() {
|
||||
it("should throw for extra keys", function() {
|
||||
expect(function() {
|
||||
utils.checkObjectHasNoAdditionalKeys({ foo: "bar", baz: 4 }, ["foo"]);
|
||||
}).toThrow();
|
||||
|
||||
expect(function() {
|
||||
utils.checkObjectHasNoAdditionalKeys({ foo: "bar" }, ["foo"]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deepCompare", function() {
|
||||
const assert = {
|
||||
isTrue: function(x) {
|
||||
expect(x).toBe(true);
|
||||
},
|
||||
isFalse: function(x) {
|
||||
expect(x).toBe(false);
|
||||
},
|
||||
};
|
||||
|
||||
it("should handle primitives", function() {
|
||||
assert.isTrue(utils.deepCompare(null, null));
|
||||
assert.isFalse(utils.deepCompare(null, undefined));
|
||||
assert.isTrue(utils.deepCompare("hi", "hi"));
|
||||
assert.isTrue(utils.deepCompare(5, 5));
|
||||
assert.isFalse(utils.deepCompare(5, 10));
|
||||
});
|
||||
|
||||
it("should handle regexps", function() {
|
||||
assert.isTrue(utils.deepCompare(/abc/, /abc/));
|
||||
assert.isFalse(utils.deepCompare(/abc/, /123/));
|
||||
const r = /abc/;
|
||||
assert.isTrue(utils.deepCompare(r, r));
|
||||
});
|
||||
|
||||
it("should handle dates", function() {
|
||||
assert.isTrue(utils.deepCompare(new Date("2011-03-31"), new Date("2011-03-31")));
|
||||
assert.isFalse(utils.deepCompare(new Date("2011-03-31"), new Date("1970-01-01")));
|
||||
});
|
||||
|
||||
it("should handle arrays", function() {
|
||||
assert.isTrue(utils.deepCompare([], []));
|
||||
assert.isTrue(utils.deepCompare([1, 2], [1, 2]));
|
||||
assert.isFalse(utils.deepCompare([1, 2], [2, 1]));
|
||||
assert.isFalse(utils.deepCompare([1, 2], [1, 2, 3]));
|
||||
});
|
||||
|
||||
it("should handle simple objects", function() {
|
||||
assert.isTrue(utils.deepCompare({}, {}));
|
||||
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 2 }));
|
||||
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { b: 2, a: 1 }));
|
||||
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 3 }));
|
||||
|
||||
assert.isTrue(utils.deepCompare({
|
||||
1: { name: "mhc", age: 28 },
|
||||
2: { name: "arb", age: 26 },
|
||||
}, {
|
||||
1: { name: "mhc", age: 28 },
|
||||
2: { name: "arb", age: 26 },
|
||||
}));
|
||||
|
||||
assert.isFalse(utils.deepCompare({
|
||||
1: { name: "mhc", age: 28 },
|
||||
2: { name: "arb", age: 26 },
|
||||
}, {
|
||||
1: { name: "mhc", age: 28 },
|
||||
2: { name: "arb", age: 27 },
|
||||
}));
|
||||
|
||||
assert.isFalse(utils.deepCompare({}, null));
|
||||
assert.isFalse(utils.deepCompare({}, undefined));
|
||||
});
|
||||
|
||||
it("should handle functions", function() {
|
||||
// no two different function is equal really, they capture their
|
||||
// context variables so even if they have same toString(), they
|
||||
// won't have same functionality
|
||||
const func = function(x) {
|
||||
return true;
|
||||
};
|
||||
const func2 = function(x) {
|
||||
return true;
|
||||
};
|
||||
assert.isTrue(utils.deepCompare(func, func));
|
||||
assert.isFalse(utils.deepCompare(func, func2));
|
||||
assert.isTrue(utils.deepCompare({ a: { b: func } }, { a: { b: func } }));
|
||||
assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("extend", function() {
|
||||
const SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
|
||||
|
||||
it("should extend", function() {
|
||||
const target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
const merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
const sourceOrig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
|
||||
});
|
||||
|
||||
it("should ignore null", function() {
|
||||
const target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
const merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
const sourceOrig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, null, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
|
||||
});
|
||||
|
||||
it("should handle properties created with defineProperties", function() {
|
||||
const source = Object.defineProperties({}, {
|
||||
"enumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
"nonenumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Fix type
|
||||
const target: any = {};
|
||||
utils.extend(target, source);
|
||||
expect(target.enumerableProp).toBe(true);
|
||||
expect(target.nonenumerableProp).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chunkPromises", function() {
|
||||
it("should execute promises in chunks", async function() {
|
||||
let promiseCount = 0;
|
||||
|
||||
async function fn1() {
|
||||
await utils.sleep(1);
|
||||
expect(promiseCount).toEqual(0);
|
||||
++promiseCount;
|
||||
}
|
||||
|
||||
async function fn2() {
|
||||
expect(promiseCount).toEqual(1);
|
||||
++promiseCount;
|
||||
}
|
||||
|
||||
await utils.chunkPromises([fn1, fn2], 1);
|
||||
expect(promiseCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DEFAULT_ALPHABET', () => {
|
||||
it('should be usefully printable ASCII in order', () => {
|
||||
expect(DEFAULT_ALPHABET).toEqual(
|
||||
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('alphabetPad', () => {
|
||||
it('should pad to the alphabet length', () => {
|
||||
const len = 12;
|
||||
expect(alphabetPad("a", len)).toEqual("a" + ("".padEnd(len - 1, DEFAULT_ALPHABET[0])));
|
||||
expect(alphabetPad("a", len, "123")).toEqual("a" + ("".padEnd(len - 1, '1')));
|
||||
});
|
||||
});
|
||||
|
||||
describe('baseToString', () => {
|
||||
it('should calculate the appropriate string from numbers', () => {
|
||||
// Verify the whole alphabet
|
||||
for (let i = BigInt(1); i <= DEFAULT_ALPHABET.length; i++) {
|
||||
logger.log({ i }); // for debugging
|
||||
expect(baseToString(i)).toEqual(DEFAULT_ALPHABET[Number(i) - 1]);
|
||||
}
|
||||
|
||||
// Just quickly double check that repeated characters aren't treated as padding, particularly
|
||||
// at the beginning of the alphabet where they are most vulnerable to this behaviour.
|
||||
expect(baseToString(BigInt(1))).toEqual(DEFAULT_ALPHABET[0].repeat(1));
|
||||
expect(baseToString(BigInt(96))).toEqual(DEFAULT_ALPHABET[0].repeat(2));
|
||||
expect(baseToString(BigInt(9121))).toEqual(DEFAULT_ALPHABET[0].repeat(3));
|
||||
expect(baseToString(BigInt(866496))).toEqual(DEFAULT_ALPHABET[0].repeat(4));
|
||||
expect(baseToString(BigInt(82317121))).toEqual(DEFAULT_ALPHABET[0].repeat(5));
|
||||
expect(baseToString(BigInt(7820126496))).toEqual(DEFAULT_ALPHABET[0].repeat(6));
|
||||
|
||||
expect(baseToString(BigInt(10))).toEqual(DEFAULT_ALPHABET[9]);
|
||||
expect(baseToString(BigInt(10), "abcdefghijklmnopqrstuvwxyz")).toEqual('j');
|
||||
expect(baseToString(BigInt(6337))).toEqual("ab");
|
||||
expect(baseToString(BigInt(80), "abcdefghijklmnopqrstuvwxyz")).toEqual('cb');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stringToBase', () => {
|
||||
it('should calculate the appropriate number for a string', () => {
|
||||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(1))).toEqual(BigInt(1));
|
||||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(2))).toEqual(BigInt(96));
|
||||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(3))).toEqual(BigInt(9121));
|
||||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(4))).toEqual(BigInt(866496));
|
||||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(5))).toEqual(BigInt(82317121));
|
||||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(6))).toEqual(BigInt(7820126496));
|
||||
expect(stringToBase("a", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(1));
|
||||
expect(stringToBase("a")).toEqual(BigInt(66));
|
||||
expect(stringToBase("c", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(3));
|
||||
expect(stringToBase("ab")).toEqual(BigInt(6337));
|
||||
expect(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(80));
|
||||
});
|
||||
});
|
||||
|
||||
describe('averageBetweenStrings', () => {
|
||||
it('should average appropriately', () => {
|
||||
expect(averageBetweenStrings(" ", "!!")).toEqual(" P");
|
||||
expect(averageBetweenStrings(" ", "!")).toEqual(" ");
|
||||
expect(averageBetweenStrings('A', 'B')).toEqual('A ');
|
||||
expect(averageBetweenStrings('AA', 'BB')).toEqual('Aq');
|
||||
expect(averageBetweenStrings('A', 'z')).toEqual(']');
|
||||
expect(averageBetweenStrings('a', 'z', "abcdefghijklmnopqrstuvwxyz")).toEqual('m');
|
||||
expect(averageBetweenStrings('AA', 'zz')).toEqual('^.');
|
||||
expect(averageBetweenStrings('aa', 'zz', "abcdefghijklmnopqrstuvwxyz")).toEqual('mz');
|
||||
expect(averageBetweenStrings('cat', 'doggo')).toEqual("d9>Cw");
|
||||
expect(averageBetweenStrings('cat', 'doggo', "abcdefghijklmnopqrstuvwxyz")).toEqual("cumqh");
|
||||
});
|
||||
});
|
||||
|
||||
describe('nextString', () => {
|
||||
it('should find the next string appropriately', () => {
|
||||
expect(nextString('A')).toEqual('B');
|
||||
expect(nextString('b', 'abcdefghijklmnopqrstuvwxyz')).toEqual('c');
|
||||
expect(nextString('cat')).toEqual('cau');
|
||||
expect(nextString('cat', 'abcdefghijklmnopqrstuvwxyz')).toEqual('cau');
|
||||
});
|
||||
});
|
||||
|
||||
describe('prevString', () => {
|
||||
it('should find the next string appropriately', () => {
|
||||
expect(prevString('B')).toEqual('A');
|
||||
expect(prevString('c', 'abcdefghijklmnopqrstuvwxyz')).toEqual('b');
|
||||
expect(prevString('cau')).toEqual('cat');
|
||||
expect(prevString('cau', 'abcdefghijklmnopqrstuvwxyz')).toEqual('cat');
|
||||
});
|
||||
});
|
||||
|
||||
// Let's just ensure the ordering is sensible for lexicographic ordering
|
||||
describe('string averaging unified', () => {
|
||||
it('should be truly previous and next', () => {
|
||||
let midpoint = "cat";
|
||||
|
||||
// We run this test 100 times to ensure we end up with a sane sequence.
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const next = nextString(midpoint);
|
||||
const prev = prevString(midpoint);
|
||||
logger.log({ i, midpoint, next, prev }); // for test debugging
|
||||
|
||||
expect(lexicographicCompare(midpoint, next) < 0).toBe(true);
|
||||
expect(lexicographicCompare(midpoint, prev) > 0).toBe(true);
|
||||
expect(averageBetweenStrings(prev, next)).toBe(midpoint);
|
||||
|
||||
midpoint = next;
|
||||
}
|
||||
});
|
||||
|
||||
it('should roll over', () => {
|
||||
const lastAlpha = DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1];
|
||||
const firstAlpha = DEFAULT_ALPHABET[0];
|
||||
|
||||
const highRoll = firstAlpha + firstAlpha;
|
||||
const lowRoll = lastAlpha;
|
||||
|
||||
expect(nextString(lowRoll)).toEqual(highRoll);
|
||||
expect(prevString(highRoll)).toEqual(lowRoll);
|
||||
});
|
||||
|
||||
it('should be reversible on small strings', () => {
|
||||
// Large scale reversibility is tested for max space order value
|
||||
const input = "cats";
|
||||
expect(prevString(nextString(input))).toEqual(input);
|
||||
});
|
||||
|
||||
// We want to explicitly make sure that Space order values are supported and roll appropriately
|
||||
it('should properly handle rolling over at 50 characters', () => {
|
||||
// Note: we also test reversibility of large strings here.
|
||||
|
||||
const maxSpaceValue = DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1].repeat(50);
|
||||
const fiftyFirstChar = DEFAULT_ALPHABET[0].repeat(51);
|
||||
|
||||
expect(nextString(maxSpaceValue)).toBe(fiftyFirstChar);
|
||||
expect(prevString(fiftyFirstChar)).toBe(maxSpaceValue);
|
||||
|
||||
// We're testing that the rollover happened, which means that the next string come before
|
||||
// the maximum space order value lexicographically.
|
||||
expect(lexicographicCompare(maxSpaceValue, fiftyFirstChar) > 0).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lexicographicCompare', () => {
|
||||
it('should work', () => {
|
||||
// Simple tests
|
||||
expect(lexicographicCompare('a', 'b') < 0).toBe(true);
|
||||
expect(lexicographicCompare('ab', 'b') < 0).toBe(true);
|
||||
expect(lexicographicCompare('cat', 'dog') < 0).toBe(true);
|
||||
|
||||
// Simple tests (reversed)
|
||||
expect(lexicographicCompare('b', 'a') > 0).toBe(true);
|
||||
expect(lexicographicCompare('b', 'ab') > 0).toBe(true);
|
||||
expect(lexicographicCompare('dog', 'cat') > 0).toBe(true);
|
||||
|
||||
// Simple equality tests
|
||||
expect(lexicographicCompare('a', 'a') === 0).toBe(true);
|
||||
expect(lexicographicCompare('A', 'A') === 0).toBe(true);
|
||||
|
||||
// ASCII rule testing
|
||||
expect(lexicographicCompare('A', 'a') < 0).toBe(true);
|
||||
expect(lexicographicCompare('a', 'A') > 0).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { MatrixCall, CallErrorCode, CallEvent } from '../../../src/webrtc/call';
|
||||
|
||||
const DUMMY_SDP = (
|
||||
"v=0\r\n" +
|
||||
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" +
|
||||
"s=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n" +
|
||||
"a=msid-semantic: WMS h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA\r\n" +
|
||||
"m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:hLDR\r\n" +
|
||||
"a=ice-pwd:bMGD9aOldHWiI+6nAq/IIlRw\r\n" +
|
||||
"a=ice-options:trickle\r\n" +
|
||||
"a=fingerprint:sha-256 E4:94:84:F9:4A:98:8A:56:F5:5F:FD:AF:72:B9:32:89:49:5C:4B:9A:" +
|
||||
"4A:15:8E:41:8A:F3:69:E4:39:52:DC:D6\r\n" +
|
||||
"a=setup:active\r\n" +
|
||||
"a=mid:0\r\n" +
|
||||
"a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" +
|
||||
"a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
|
||||
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
|
||||
"a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n" +
|
||||
"a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n" +
|
||||
"a=sendrecv\r\n" +
|
||||
"a=msid:h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA 4357098f-3795-4131-bff4-9ba9c0348c49\r\n" +
|
||||
"a=rtcp-mux\r\n" +
|
||||
"a=rtpmap:111 opus/48000/2\r\n" +
|
||||
"a=rtcp-fb:111 transport-cc\r\n" +
|
||||
"a=fmtp:111 minptime=10;useinbandfec=1\r\n" +
|
||||
"a=rtpmap:103 ISAC/16000\r\n" +
|
||||
"a=rtpmap:104 ISAC/32000\r\n" +
|
||||
"a=rtpmap:9 G722/8000\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=rtpmap:8 PCMA/8000\r\n" +
|
||||
"a=rtpmap:106 CN/32000\r\n" +
|
||||
"a=rtpmap:105 CN/16000\r\n" +
|
||||
"a=rtpmap:13 CN/8000\r\n" +
|
||||
"a=rtpmap:110 telephone-event/48000\r\n" +
|
||||
"a=rtpmap:112 telephone-event/32000\r\n" +
|
||||
"a=rtpmap:113 telephone-event/16000\r\n" +
|
||||
"a=rtpmap:126 telephone-event/8000\r\n" +
|
||||
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
|
||||
);
|
||||
|
||||
class MockRTCPeerConnection {
|
||||
localDescription: RTCSessionDescription;
|
||||
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
sdp: DUMMY_SDP,
|
||||
type: 'offer',
|
||||
toJSON: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
addEventListener() {}
|
||||
createOffer() {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
setRemoteDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
setLocalDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
close() {}
|
||||
getStats() { return []; }
|
||||
}
|
||||
|
||||
describe('Call', function() {
|
||||
let client;
|
||||
let call;
|
||||
let prevNavigator;
|
||||
let prevDocument;
|
||||
let prevWindow;
|
||||
|
||||
beforeEach(function() {
|
||||
prevNavigator = global.navigator;
|
||||
prevDocument = global.document;
|
||||
prevWindow = global.window;
|
||||
|
||||
global.navigator = {
|
||||
mediaDevices: {
|
||||
// @ts-ignore Mock
|
||||
getUserMedia: () => {
|
||||
return {
|
||||
getTracks: () => [],
|
||||
getAudioTracks: () => [],
|
||||
getVideoTracks: () => [],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
global.window = {
|
||||
// @ts-ignore Mock
|
||||
RTCPeerConnection: MockRTCPeerConnection,
|
||||
// @ts-ignore Mock
|
||||
RTCSessionDescription: {},
|
||||
// @ts-ignore Mock
|
||||
RTCIceCandidate: {},
|
||||
getUserMedia: {},
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
global.document = {};
|
||||
|
||||
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {});
|
||||
// We just stub out sendEvent: we're not interested in testing the client's
|
||||
// event sending code here
|
||||
client.client.sendEvent = () => {};
|
||||
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
|
||||
call = new MatrixCall({
|
||||
client: client.client,
|
||||
roomId: '!foo:bar',
|
||||
});
|
||||
// call checks one of these is wired up
|
||||
call.on('error', () => {});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
client.stop();
|
||||
global.navigator = prevNavigator;
|
||||
global.window = prevWindow;
|
||||
global.document = prevDocument;
|
||||
});
|
||||
|
||||
it('should ignore candidate events from non-matching party ID', async function() {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'the_correct_party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
call.peerConn.addIceCandidate = jest.fn();
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'the_correct_party_id',
|
||||
candidates: [
|
||||
{
|
||||
candidate: '',
|
||||
sdpMid: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
|
||||
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'some_other_party_id',
|
||||
candidates: [
|
||||
{
|
||||
candidate: '',
|
||||
sdpMid: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
|
||||
|
||||
// Hangup to stop timers
|
||||
call.hangup(CallErrorCode.UserHangup, true);
|
||||
});
|
||||
|
||||
it('should add candidates received before answer if party ID is correct', async function() {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
call.peerConn.addIceCandidate = jest.fn();
|
||||
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'the_correct_party_id',
|
||||
candidates: [
|
||||
{
|
||||
candidate: 'the_correct_candidate',
|
||||
sdpMid: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'some_other_party_id',
|
||||
candidates: [
|
||||
{
|
||||
candidate: 'the_wrong_candidate',
|
||||
sdpMid: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(0);
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'the_correct_party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
|
||||
expect(call.peerConn.addIceCandidate).toHaveBeenCalledWith({
|
||||
candidate: 'the_correct_candidate',
|
||||
sdpMid: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should map asserted identity messages to remoteAssertedIdentity', async function() {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const identChangedCallback = jest.fn();
|
||||
call.on(CallEvent.AssertedIdentityChanged, identChangedCallback);
|
||||
|
||||
await call.onAssertedIdentityReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
asserted_identity: {
|
||||
id: "@steve:example.com",
|
||||
display_name: "Steve Gibbons",
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(identChangedCallback).toHaveBeenCalled();
|
||||
|
||||
const ident = call.getRemoteAssertedIdentity();
|
||||
expect(ident.id).toEqual("@steve:example.com");
|
||||
expect(ident.displayName).toEqual("Steve Gibbons");
|
||||
|
||||
// Hangup to stop timers
|
||||
call.hangup(CallErrorCode.UserHangup, true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
|
||||
export interface IIdentityServerProvider {
|
||||
/**
|
||||
* Gets an access token for use against the identity server,
|
||||
* for the associated client.
|
||||
* @returns {Promise<string>} Resolves to the access token.
|
||||
*/
|
||||
getAccessToken(): Promise<string>;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
|
||||
export enum EventType {
|
||||
// Room state events
|
||||
RoomCanonicalAlias = "m.room.canonical_alias",
|
||||
RoomCreate = "m.room.create",
|
||||
RoomJoinRules = "m.room.join_rules",
|
||||
RoomMember = "m.room.member",
|
||||
RoomThirdPartyInvite = "m.room.third_party_invite",
|
||||
RoomPowerLevels = "m.room.power_levels",
|
||||
RoomName = "m.room.name",
|
||||
RoomTopic = "m.room.topic",
|
||||
RoomAvatar = "m.room.avatar",
|
||||
RoomPinnedEvents = "m.room.pinned_events",
|
||||
RoomEncryption = "m.room.encryption",
|
||||
RoomHistoryVisibility = "m.room.history_visibility",
|
||||
RoomGuestAccess = "m.room.guest_access",
|
||||
RoomServerAcl = "m.room.server_acl",
|
||||
RoomTombstone = "m.room.tombstone",
|
||||
/**
|
||||
* @deprecated Should not be used.
|
||||
*/
|
||||
RoomAliases = "m.room.aliases", // deprecated https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
|
||||
|
||||
SpaceChild = "m.space.child",
|
||||
SpaceParent = "m.space.parent",
|
||||
|
||||
// Room timeline events
|
||||
RoomRedaction = "m.room.redaction",
|
||||
RoomMessage = "m.room.message",
|
||||
RoomMessageEncrypted = "m.room.encrypted",
|
||||
Sticker = "m.sticker",
|
||||
CallInvite = "m.call.invite",
|
||||
CallCandidates = "m.call.candidates",
|
||||
CallAnswer = "m.call.answer",
|
||||
CallHangup = "m.call.hangup",
|
||||
CallReject = "m.call.reject",
|
||||
CallSelectAnswer = "m.call.select_answer",
|
||||
CallNegotiate = "m.call.negotiate",
|
||||
CallReplaces = "m.call.replaces",
|
||||
CallAssertedIdentity = "m.call.asserted_identity",
|
||||
CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity",
|
||||
KeyVerificationRequest = "m.key.verification.request",
|
||||
KeyVerificationStart = "m.key.verification.start",
|
||||
KeyVerificationCancel = "m.key.verification.cancel",
|
||||
KeyVerificationMac = "m.key.verification.mac",
|
||||
KeyVerificationDone = "m.key.verification.done",
|
||||
// use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback
|
||||
RoomMessageFeedback = "m.room.message.feedback",
|
||||
Reaction = "m.reaction",
|
||||
|
||||
// Room ephemeral events
|
||||
Typing = "m.typing",
|
||||
Receipt = "m.receipt",
|
||||
Presence = "m.presence",
|
||||
|
||||
// Room account_data events
|
||||
FullyRead = "m.fully_read",
|
||||
Tag = "m.tag",
|
||||
SpaceOrder = "org.matrix.msc3230.space_order", // MSC3230
|
||||
|
||||
// User account_data events
|
||||
PushRules = "m.push_rules",
|
||||
Direct = "m.direct",
|
||||
IgnoredUserList = "m.ignored_user_list",
|
||||
|
||||
// to_device events
|
||||
RoomKey = "m.room_key",
|
||||
RoomKeyRequest = "m.room_key_request",
|
||||
ForwardedRoomKey = "m.forwarded_room_key",
|
||||
Dummy = "m.dummy",
|
||||
}
|
||||
|
||||
export enum MsgType {
|
||||
Text = "m.text",
|
||||
Emote = "m.emote",
|
||||
Notice = "m.notice",
|
||||
Image = "m.image",
|
||||
File = "m.file",
|
||||
Audio = "m.audio",
|
||||
Location = "m.location",
|
||||
Video = "m.video",
|
||||
}
|
||||
|
||||
export const RoomCreateTypeField = "type";
|
||||
|
||||
export enum RoomType {
|
||||
Space = "m.space",
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifier for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088)
|
||||
* room purpose. Note that this reference is UNSTABLE and subject to breaking changes,
|
||||
* including its eventual removal.
|
||||
*/
|
||||
export const UNSTABLE_MSC3088_PURPOSE = new UnstableValue("m.room.purpose", "org.matrix.msc3088.purpose");
|
||||
|
||||
/**
|
||||
* Enabled flag for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088)
|
||||
* room purpose. Note that this reference is UNSTABLE and subject to breaking changes,
|
||||
* including its eventual removal.
|
||||
*/
|
||||
export const UNSTABLE_MSC3088_ENABLED = new UnstableValue("m.enabled", "org.matrix.msc3088.enabled");
|
||||
|
||||
/**
|
||||
* Subtype for an [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room.
|
||||
* Note that this reference is UNSTABLE and subject to breaking changes, including its
|
||||
* eventual removal.
|
||||
*/
|
||||
export const UNSTABLE_MSC3089_TREE_SUBTYPE = new UnstableValue("m.data_tree", "org.matrix.msc3089.data_tree");
|
||||
|
||||
/**
|
||||
* Leaf type for an event in a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room.
|
||||
* Note that this reference is UNSTABLE and subject to breaking changes, including its
|
||||
* eventual removal.
|
||||
*/
|
||||
export const UNSTABLE_MSC3089_LEAF = new UnstableValue("m.leaf", "org.matrix.msc3089.leaf");
|
||||
|
||||
/**
|
||||
* Branch (Leaf Reference) type for the index approach in a
|
||||
* [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room. Note that this reference is
|
||||
* UNSTABLE and subject to breaking changes, including its eventual removal.
|
||||
*/
|
||||
export const UNSTABLE_MSC3089_BRANCH = new UnstableValue("m.branch", "org.matrix.msc3089.branch");
|
||||
|
||||
export interface IEncryptedFile {
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
key: {
|
||||
alg: string;
|
||||
key_ops: string[]; // eslint-disable-line camelcase
|
||||
kty: string;
|
||||
k: string;
|
||||
ext: boolean;
|
||||
};
|
||||
iv: string;
|
||||
hashes: {[alg: string]: string};
|
||||
v: string;
|
||||
}
|
||||
Vendored
+60
-1
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// this is needed to tell TS about global.Olm
|
||||
import * as Olm from "olm"; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
import * as Olm from "@matrix-org/olm"; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
|
||||
export {};
|
||||
|
||||
@@ -25,4 +25,63 @@ declare global {
|
||||
localStorage: Storage;
|
||||
}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
electron?: Electron;
|
||||
}
|
||||
|
||||
interface Electron {
|
||||
getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>>;
|
||||
}
|
||||
|
||||
interface MediaDevices {
|
||||
// This is experimental and types don't know about it yet
|
||||
// https://github.com/microsoft/TypeScript/issues/33232
|
||||
getDisplayMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
|
||||
getUserMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
|
||||
}
|
||||
|
||||
interface DesktopCapturerConstraints {
|
||||
audio: boolean | {
|
||||
mandatory: {
|
||||
chromeMediaSource: string;
|
||||
chromeMediaSourceId: string;
|
||||
};
|
||||
};
|
||||
video: boolean | {
|
||||
mandatory: {
|
||||
chromeMediaSource: string;
|
||||
chromeMediaSourceId: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface DesktopCapturerSource {
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnailURL: string;
|
||||
}
|
||||
|
||||
interface GetSourcesOptions {
|
||||
types: Array<string>;
|
||||
thumbnailSize?: {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
fetchWindowIcons?: boolean;
|
||||
}
|
||||
|
||||
interface HTMLAudioElement {
|
||||
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||
sinkId: string;
|
||||
setSinkId(outputId: string);
|
||||
}
|
||||
|
||||
interface DummyInterfaceWeShouldntBeUsingThis {}
|
||||
|
||||
interface Navigator {
|
||||
// We check for the webkit-prefixed getUserMedia to detect if we're
|
||||
// on webkit: we should check if we still need to do this
|
||||
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
|
||||
export interface IImageInfo {
|
||||
size?: number;
|
||||
mimetype?: string;
|
||||
thumbnail_info?: { // eslint-disable-line camelcase
|
||||
w?: number;
|
||||
h?: number;
|
||||
size?: number;
|
||||
mimetype?: string;
|
||||
};
|
||||
w?: number;
|
||||
h?: number;
|
||||
}
|
||||
|
||||
export enum Visibility {
|
||||
Public = "public",
|
||||
Private = "private",
|
||||
}
|
||||
|
||||
export enum Preset {
|
||||
PrivateChat = "private_chat",
|
||||
TrustedPrivateChat = "trusted_private_chat",
|
||||
PublicChat = "public_chat",
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
Copyright 2021 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 { Callback } from "../client";
|
||||
import { Preset, Visibility } from "./partials";
|
||||
|
||||
// allow camelcase as these are things go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export interface IJoinRoomOpts {
|
||||
/**
|
||||
* True to do a room initial sync on the resulting
|
||||
* room. If false, the <strong>returned Room object will have no current state.
|
||||
* </strong> Default: true.
|
||||
*/
|
||||
syncRoom?: boolean;
|
||||
|
||||
/**
|
||||
* If the caller has a keypair 3pid invite, the signing URL is passed in this parameter.
|
||||
*/
|
||||
inviteSignUrl?: string;
|
||||
|
||||
/**
|
||||
* The server names to try and join through in addition to those that are automatically chosen.
|
||||
*/
|
||||
viaServers?: string[];
|
||||
}
|
||||
|
||||
export interface IRedactOpts {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface ISendEventResponse {
|
||||
event_id: string;
|
||||
}
|
||||
|
||||
export interface IPresenceOpts {
|
||||
presence: "online" | "offline" | "unavailable";
|
||||
status_msg?: string;
|
||||
}
|
||||
|
||||
export interface IPaginateOpts {
|
||||
backwards?: boolean;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface IGuestAccessOpts {
|
||||
allowJoin: boolean;
|
||||
allowRead: boolean;
|
||||
}
|
||||
|
||||
export interface ISearchOpts {
|
||||
keys?: string[];
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface IEventSearchOpts {
|
||||
filter: any; // TODO: Types
|
||||
term: string;
|
||||
}
|
||||
|
||||
export interface IInvite3PID {
|
||||
id_server: string;
|
||||
id_access_token?: string; // this gets injected by the js-sdk
|
||||
medium: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface ICreateRoomStateEvent {
|
||||
type: string;
|
||||
state_key?: string; // defaults to an empty string
|
||||
content: object;
|
||||
}
|
||||
|
||||
export interface ICreateRoomOpts {
|
||||
room_alias_name?: string;
|
||||
visibility?: Visibility;
|
||||
name?: string;
|
||||
topic?: string;
|
||||
preset?: Preset;
|
||||
power_level_content_override?: object;
|
||||
creation_content?: object;
|
||||
initial_state?: ICreateRoomStateEvent[];
|
||||
invite?: string[];
|
||||
invite_3pid?: IInvite3PID[];
|
||||
is_direct?: boolean;
|
||||
room_version?: string;
|
||||
}
|
||||
|
||||
export interface IRoomDirectoryOptions {
|
||||
server?: string;
|
||||
limit?: number;
|
||||
since?: string;
|
||||
|
||||
// TODO: Proper types
|
||||
filter?: any & {generic_search_term: string};
|
||||
}
|
||||
|
||||
export interface IUploadOpts {
|
||||
name?: string;
|
||||
includeFilename?: boolean;
|
||||
type?: string;
|
||||
rawResponse?: boolean;
|
||||
onlyContentUri?: boolean;
|
||||
callback?: Callback;
|
||||
progressHandler?: (state: {loaded: number, total: number}) => void;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
|
||||
export interface ISignatures {
|
||||
[entity: string]: {
|
||||
[keyId: string]: string;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a simple Matrix namespaced value. This will assume that if a stable prefix
|
||||
* is provided that the stable prefix should be used when representing the identifier.
|
||||
*/
|
||||
export class NamespacedValue<S extends string, U extends string> {
|
||||
// Stable is optional, but one of the two parameters is required, hence the weird-looking types.
|
||||
// Goal is to to have developers explicitly say there is no stable value (if applicable).
|
||||
public constructor(public readonly stable: S | null | undefined, public readonly unstable?: U) {
|
||||
if (!this.unstable && !this.stable) {
|
||||
throw new Error("One of stable or unstable values must be supplied");
|
||||
}
|
||||
}
|
||||
|
||||
public get name(): U | S {
|
||||
if (this.stable) {
|
||||
return this.stable;
|
||||
}
|
||||
return this.unstable;
|
||||
}
|
||||
|
||||
public get altName(): U | S | null {
|
||||
if (!this.stable) {
|
||||
return null;
|
||||
}
|
||||
return this.unstable;
|
||||
}
|
||||
|
||||
public matches(val: string): boolean {
|
||||
return this.name === val || this.altName === val;
|
||||
}
|
||||
|
||||
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
|
||||
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
|
||||
public findIn<T>(obj: any): T {
|
||||
let val: T;
|
||||
if (this.name) {
|
||||
val = obj?.[this.name];
|
||||
}
|
||||
if (!val && this.altName) {
|
||||
val = obj?.[this.altName];
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
public includedIn(arr: any[]): boolean {
|
||||
let included = false;
|
||||
if (this.name) {
|
||||
included = arr.includes(this.name);
|
||||
}
|
||||
if (!included && this.altName) {
|
||||
included = arr.includes(this.altName);
|
||||
}
|
||||
return included;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a namespaced value which prioritizes the unstable value over the stable
|
||||
* value.
|
||||
*/
|
||||
export class UnstableValue<S extends string, U extends string> extends NamespacedValue<S, U> {
|
||||
// Note: Constructor difference is that `unstable` is *required*.
|
||||
public constructor(stable: S, unstable: U) {
|
||||
super(stable, unstable);
|
||||
if (!this.unstable) {
|
||||
throw new Error("Unstable value must be supplied");
|
||||
}
|
||||
}
|
||||
|
||||
public get name(): U {
|
||||
return this.unstable;
|
||||
}
|
||||
|
||||
public get altName(): S {
|
||||
return this.stable;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module
|
||||
*/
|
||||
|
||||
export class ReEmitter {
|
||||
constructor(target) {
|
||||
this.target = target;
|
||||
|
||||
// We keep one bound event handler for each event name so we know
|
||||
// what event is arriving
|
||||
this.boundHandlers = {};
|
||||
}
|
||||
|
||||
_handleEvent(eventName, ...args) {
|
||||
this.target.emit(eventName, ...args);
|
||||
}
|
||||
|
||||
reEmit(source, eventNames) {
|
||||
// We include the source as the last argument for event handlers which may need it,
|
||||
// such as read receipt listeners on the client class which won't have the context
|
||||
// of the room.
|
||||
const forSource = (handler, ...args) => {
|
||||
handler(...args, source);
|
||||
};
|
||||
for (const eventName of eventNames) {
|
||||
if (this.boundHandlers[eventName] === undefined) {
|
||||
this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName);
|
||||
}
|
||||
|
||||
const boundHandler = forSource.bind(this, this.boundHandlers[eventName]);
|
||||
source.on(eventName, boundHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 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.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
export class ReEmitter {
|
||||
private target: EventEmitter;
|
||||
|
||||
constructor(target: EventEmitter) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
reEmit(source: EventEmitter, eventNames: string[]) {
|
||||
for (const eventName of eventNames) {
|
||||
// We include the source as the last argument for event handlers which may need it,
|
||||
// such as read receipt listeners on the client class which won't have the context
|
||||
// of the room.
|
||||
const forSource = (...args) => {
|
||||
// EventEmitter special cases 'error' to make the emit function throw if no
|
||||
// handler is attached, which sort of makes sense for making sure that something
|
||||
// handles an error, but for re-emitting, there could be a listener on the original
|
||||
// source object so the test doesn't really work. We *could* try to replicate the
|
||||
// same logic and throw if there is no listener on either the source or the target,
|
||||
// but this behaviour is fairly undesireable for us anyway: the main place we throw
|
||||
// 'error' events is for calls, where error events are usually emitted some time
|
||||
// later by a different part of the code where 'emit' throwing because the app hasn't
|
||||
// added an error handler isn't terribly helpful. (A better fix in retrospect may
|
||||
// have been to just avoid using the event name 'error', but backwards compat...)
|
||||
if (eventName === 'error' && this.target.listenerCount('error') === 0) return;
|
||||
this.target.emit(eventName, ...args, source);
|
||||
};
|
||||
source.on(eventName, forSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@ limitations under the License.
|
||||
|
||||
/** @module auto-discovery */
|
||||
|
||||
import {logger} from './logger';
|
||||
import {URL as NodeURL} from "url";
|
||||
import { logger } from './logger';
|
||||
import { URL as NodeURL } from "url";
|
||||
|
||||
// Dev note: Auto discovery is part of the spec.
|
||||
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
||||
@@ -511,12 +511,12 @@ export class AutoDiscovery {
|
||||
action = "IGNORE";
|
||||
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
|
||||
}
|
||||
resolve({raw: {}, action: action, reason: reason, error: err});
|
||||
resolve({ raw: {}, action: action, reason: reason, error: err });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
resolve({raw: JSON.parse(body), action: "SUCCESS"});
|
||||
resolve({ raw: JSON.parse(body), action: "SUCCESS" });
|
||||
} catch (e) {
|
||||
let reason = AutoDiscovery.ERROR_INVALID;
|
||||
if (e.name === "SyntaxError") {
|
||||
|
||||
-2369
File diff suppressed because it is too large
Load Diff
-6057
File diff suppressed because it is too large
Load Diff
+8472
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -59,7 +59,7 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height,
|
||||
if (resizeMethod) {
|
||||
params.method = resizeMethod;
|
||||
}
|
||||
if (utils.keys(params).length > 0) {
|
||||
if (Object.keys(params).length > 0) {
|
||||
// these are thumbnailing params so they probably want the
|
||||
// thumbnailing API...
|
||||
prefix = "/_matrix/media/r0/thumbnail/";
|
||||
@@ -72,6 +72,6 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height,
|
||||
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
|
||||
}
|
||||
return baseUrl + prefix + serverAndMediaId +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
(Object.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params))) + fragment;
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ limitations under the License.
|
||||
* @module crypto/CrossSigning
|
||||
*/
|
||||
|
||||
import {decodeBase64, encodeBase64, pkSign, pkVerify} from './olmlib';
|
||||
import {EventEmitter} from 'events';
|
||||
import {logger} from '../logger';
|
||||
import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store';
|
||||
import {decryptAES, encryptAES} from './aes';
|
||||
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
|
||||
import { EventEmitter } from 'events';
|
||||
import { logger } from '../logger';
|
||||
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
|
||||
import { decryptAES, encryptAES } from './aes';
|
||||
|
||||
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
|
||||
|
||||
@@ -725,7 +725,7 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) {
|
||||
/**
|
||||
* Request cross-signing keys from another device during verification.
|
||||
*
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base Matrix API interface
|
||||
* @param {MatrixClient} baseApis base Matrix API interface
|
||||
* @param {string} userId The user ID being verified
|
||||
* @param {string} deviceId The device ID being verified
|
||||
*/
|
||||
@@ -739,7 +739,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
|
||||
// it. We return here in order to test.
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = baseApis;
|
||||
const original = client._crypto._crossSigningInfo;
|
||||
const original = client.crypto._crossSigningInfo;
|
||||
|
||||
// We already have all of the infrastructure we need to validate and
|
||||
// cache cross-signing keys, so instead of replicating that, here we set
|
||||
@@ -775,7 +775,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
|
||||
|
||||
// also request and cache the key backup key
|
||||
const backupKeyPromise = new Promise(async resolve => {
|
||||
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
|
||||
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
|
||||
if (!cachedKey) {
|
||||
logger.info("No cached backup key found. Requesting...");
|
||||
const secretReq = client.requestSecret(
|
||||
@@ -785,7 +785,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
|
||||
logger.info("Got key backup key, decoding...");
|
||||
const decodedKey = decodeBase64(base64Key);
|
||||
logger.info("Decoded backup key, storing...");
|
||||
client._crypto.storeSessionBackupPrivateKey(
|
||||
client.crypto.storeSessionBackupPrivateKey(
|
||||
Uint8Array.from(decodedKey),
|
||||
);
|
||||
logger.info("Backup key stored. Starting backup restore...");
|
||||
|
||||
+21
-17
@@ -22,14 +22,13 @@ limitations under the License.
|
||||
* Manages the list of other users' devices
|
||||
*/
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
import {logger} from '../logger';
|
||||
import {DeviceInfo} from './deviceinfo';
|
||||
import {CrossSigningInfo} from './CrossSigning';
|
||||
import { EventEmitter } from 'events';
|
||||
import { logger } from '../logger';
|
||||
import { DeviceInfo } from './deviceinfo';
|
||||
import { CrossSigningInfo } from './CrossSigning';
|
||||
import * as olmlib from './olmlib';
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import {defer, sleep} from '../utils';
|
||||
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { chunkPromises, defer, sleep } from '../utils';
|
||||
|
||||
/* State transition diagram for DeviceList._deviceTrackingStatus
|
||||
*
|
||||
@@ -51,7 +50,6 @@ import {defer, sleep} from '../utils';
|
||||
* +----------------------- UP_TO_DATE ------------------------+
|
||||
*/
|
||||
|
||||
|
||||
// constants for DeviceList._deviceTrackingStatus
|
||||
const TRACKING_STATUS_NOT_TRACKED = 0;
|
||||
const TRACKING_STATUS_PENDING_DOWNLOAD = 1;
|
||||
@@ -62,7 +60,7 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
|
||||
* @alias module:crypto/DeviceList
|
||||
*/
|
||||
export class DeviceList extends EventEmitter {
|
||||
constructor(baseApis, cryptoStore, olmDevice) {
|
||||
constructor(baseApis, cryptoStore, olmDevice, keyDownloadChunkSize = 250) {
|
||||
super();
|
||||
|
||||
this._cryptoStore = cryptoStore;
|
||||
@@ -98,6 +96,9 @@ export class DeviceList extends EventEmitter {
|
||||
// userId -> promise
|
||||
this._keyDownloadsInProgressByUser = {};
|
||||
|
||||
// Maximum number of user IDs per request to prevent server overload (#1619)
|
||||
this._keyDownloadChunkSize = keyDownloadChunkSize;
|
||||
|
||||
// Set whenever changes are made other than setting the sync token
|
||||
this._dirty = false;
|
||||
|
||||
@@ -780,13 +781,17 @@ class DeviceListUpdateSerialiser {
|
||||
opts.token = this._syncToken;
|
||||
}
|
||||
|
||||
this._baseApis.downloadKeysForUsers(
|
||||
downloadUsers, opts,
|
||||
).then(async (res) => {
|
||||
const dk = res.device_keys || {};
|
||||
const masterKeys = res.master_keys || {};
|
||||
const ssks = res.self_signing_keys || {};
|
||||
const usks = res.user_signing_keys || {};
|
||||
const factories = [];
|
||||
for (let i = 0; i < downloadUsers.length; i += this._deviceList._keyDownloadChunkSize) {
|
||||
const userSlice = downloadUsers.slice(i, i + this._deviceList._keyDownloadChunkSize);
|
||||
factories.push(() => this._baseApis.downloadKeysForUsers(userSlice, opts));
|
||||
}
|
||||
|
||||
chunkPromises(factories, 3).then(async (responses) => {
|
||||
const dk = Object.assign({}, ...(responses.map(res => res.device_keys || {})));
|
||||
const masterKeys = Object.assign({}, ...(responses.map(res => res.master_keys || {})));
|
||||
const ssks = Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
|
||||
const usks = Object.assign({}, ...(responses.map(res => res.user_signing_keys || {})));
|
||||
|
||||
// yield to other things that want to execute in between users, to
|
||||
// avoid wedging the CPU
|
||||
@@ -885,7 +890,6 @@ class DeviceListUpdateSerialiser {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function _updateStoredDeviceKeysForUser(
|
||||
_olmDevice, userId, userStore, userResult, localUserId, localDeviceId,
|
||||
) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { logger } from "../logger";
|
||||
import {MatrixEvent} from "../models/event";
|
||||
import {EventEmitter} from "events";
|
||||
import {createCryptoStoreCacheCallbacks} from "./CrossSigning";
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { EventEmitter } from "events";
|
||||
import { createCryptoStoreCacheCallbacks } from "./CrossSigning";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import {
|
||||
PREFIX_UNSTABLE,
|
||||
} from "../http-api";
|
||||
@@ -43,7 +43,7 @@ export class EncryptionSetupBuilder {
|
||||
* @param {Object} keys the new keys
|
||||
*/
|
||||
addCrossSigningKeys(authUpload, keys) {
|
||||
this._crossSigningKeys = {authUpload, keys};
|
||||
this._crossSigningKeys = { authUpload, keys };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,7 +86,6 @@ export class EncryptionSetupBuilder {
|
||||
userSignatures[deviceId] = signature;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @param {Object} content
|
||||
@@ -205,27 +204,26 @@ export class EncryptionSetupOperation {
|
||||
// The backup is trusted because the user provided the private key.
|
||||
// Sign the backup with the cross signing key so the key backup can
|
||||
// be trusted via cross-signing.
|
||||
await baseApis._http.authedRequest(
|
||||
await baseApis.http.authedRequest(
|
||||
undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version,
|
||||
undefined, {
|
||||
algorithm: this._keyBackupInfo.algorithm,
|
||||
auth_data: this._keyBackupInfo.auth_data,
|
||||
},
|
||||
{prefix: PREFIX_UNSTABLE},
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
);
|
||||
} else {
|
||||
// add new key backup
|
||||
await baseApis._http.authedRequest(
|
||||
await baseApis.http.authedRequest(
|
||||
undefined, "POST", "/room_keys/version",
|
||||
undefined, this._keyBackupInfo,
|
||||
{prefix: PREFIX_UNSTABLE},
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Catches account data set by SecretStorage during bootstrapping by
|
||||
* implementing the methods related to account data in MatrixClient
|
||||
@@ -276,7 +274,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
||||
// as SecretStorage listens for it while calling this method
|
||||
// and it seems to rely on this.
|
||||
return Promise.resolve().then(() => {
|
||||
const event = new MatrixEvent({type, content});
|
||||
const event = new MatrixEvent({ type, content });
|
||||
this.emit("accountData", event, lastEvent);
|
||||
});
|
||||
}
|
||||
@@ -335,7 +333,7 @@ class SSSSCryptoCallbacks {
|
||||
// for it to the general crypto callbacks and cache it
|
||||
if (this._delegateCryptoCallbacks) {
|
||||
const result = await this._delegateCryptoCallbacks.
|
||||
getSecretStorageKey({keys}, name);
|
||||
getSecretStorageKey({ keys }, name);
|
||||
if (result) {
|
||||
const [keyId, privateKey] = result;
|
||||
this._privateKeys.set(keyId, privateKey);
|
||||
@@ -344,14 +342,14 @@ class SSSSCryptoCallbacks {
|
||||
}
|
||||
}
|
||||
|
||||
addPrivateKey(keyId, privKey) {
|
||||
addPrivateKey(keyId, keyInfo, privKey) {
|
||||
this._privateKeys.set(keyId, privKey);
|
||||
// Also pass along to application to cache if it wishes
|
||||
if (
|
||||
this._delegateCryptoCallbacks &&
|
||||
this._delegateCryptoCallbacks.cacheSecretStorageKey
|
||||
) {
|
||||
this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, privKey);
|
||||
this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, keyInfo, privKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+56
-22
@@ -16,8 +16,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from '../logger';
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import { logger } from '../logger';
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import * as algorithms from './algorithms';
|
||||
|
||||
// The maximum size of an event is 65K, and we base64 the content, so this is a
|
||||
@@ -48,7 +48,6 @@ function checkPayloadLength(payloadString) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The type of object we use for importing and exporting megolm session data.
|
||||
*
|
||||
@@ -62,7 +61,6 @@ function checkPayloadLength(payloadString) {
|
||||
* @property {String} session_key Base64'ed key data
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Manages the olm cryptography functions. Each OlmDevice has a single
|
||||
* OlmAccount and a number of OlmSessions.
|
||||
@@ -350,7 +348,7 @@ OlmDevice.prototype._unpickleSession = function(sessionInfo, func) {
|
||||
const session = new global.Olm.Session();
|
||||
try {
|
||||
session.unpickle(this._pickleKey, sessionInfo.session);
|
||||
const unpickledSessInfo = Object.assign({}, sessionInfo, {session});
|
||||
const unpickledSessInfo = Object.assign({}, sessionInfo, { session });
|
||||
|
||||
func(unpickledSessInfo);
|
||||
} finally {
|
||||
@@ -376,7 +374,6 @@ OlmDevice.prototype._saveSession = function(deviceKey, sessionInfo, txn) {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* get an OlmUtility and call the given function
|
||||
*
|
||||
@@ -393,7 +390,6 @@ OlmDevice.prototype._getUtility = function(func) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Signs a message with the ed25519 key for this account.
|
||||
*
|
||||
@@ -434,7 +430,6 @@ OlmDevice.prototype.getOneTimeKeys = async function() {
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the maximum number of one-time keys we can store.
|
||||
*
|
||||
@@ -545,11 +540,11 @@ OlmDevice.prototype.createOutboundSession = async function(
|
||||
}
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[createOutboundSession]"),
|
||||
);
|
||||
return newSessionId;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Generate a new inbound session, given an incoming message
|
||||
*
|
||||
@@ -605,12 +600,12 @@ OlmDevice.prototype.createInboundSession = async function(
|
||||
}
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[createInboundSession]"),
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get a list of known session IDs for the given device
|
||||
*
|
||||
@@ -619,8 +614,10 @@ OlmDevice.prototype.createInboundSession = async function(
|
||||
* @return {Promise<string[]>} a list of known session ids for the device
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
|
||||
const log = logger.withPrefix("[getSessionIdsForDevice]");
|
||||
|
||||
if (this._sessionsInProgress[theirDeviceIdentityKey]) {
|
||||
logger.log("waiting for olm session to be created");
|
||||
log.debug(`Waiting for Olm session for ${theirDeviceIdentityKey} to be created`);
|
||||
try {
|
||||
await this._sessionsInProgress[theirDeviceIdentityKey];
|
||||
} catch (e) {
|
||||
@@ -638,6 +635,7 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
|
||||
},
|
||||
);
|
||||
},
|
||||
log,
|
||||
);
|
||||
|
||||
return sessionIds;
|
||||
@@ -651,13 +649,14 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
|
||||
* @param {boolean} nowait Don't wait for an in-progress session to complete.
|
||||
* This should only be set to true of the calling function is the function
|
||||
* that marked the session as being in-progress.
|
||||
* @param {Logger} [log] A possibly customised log
|
||||
* @return {Promise<?string>} session id, or null if no established session
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdForDevice = async function(
|
||||
theirDeviceIdentityKey, nowait,
|
||||
theirDeviceIdentityKey, nowait, log,
|
||||
) {
|
||||
const sessionInfos = await this.getSessionInfoForDevice(
|
||||
theirDeviceIdentityKey, nowait,
|
||||
theirDeviceIdentityKey, nowait, log,
|
||||
);
|
||||
|
||||
if (sessionInfos.length === 0) {
|
||||
@@ -697,11 +696,16 @@ OlmDevice.prototype.getSessionIdForDevice = async function(
|
||||
* @param {boolean} nowait Don't wait for an in-progress session to complete.
|
||||
* This should only be set to true of the calling function is the function
|
||||
* that marked the session as being in-progress.
|
||||
* @param {Logger} [log] A possibly customised log
|
||||
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
|
||||
*/
|
||||
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey, nowait) {
|
||||
OlmDevice.prototype.getSessionInfoForDevice = async function(
|
||||
deviceIdentityKey, nowait, log = logger,
|
||||
) {
|
||||
log = log.withPrefix("[getSessionInfoForDevice]");
|
||||
|
||||
if (this._sessionsInProgress[deviceIdentityKey] && !nowait) {
|
||||
logger.log("waiting for olm session to be created");
|
||||
log.debug(`Waiting for Olm session for ${deviceIdentityKey} to be created`);
|
||||
try {
|
||||
await this._sessionsInProgress[deviceIdentityKey];
|
||||
} catch (e) {
|
||||
@@ -727,6 +731,7 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey,
|
||||
}
|
||||
});
|
||||
},
|
||||
log,
|
||||
);
|
||||
|
||||
return info;
|
||||
@@ -761,6 +766,7 @@ OlmDevice.prototype.encryptMessage = async function(
|
||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[encryptMessage]"),
|
||||
);
|
||||
return res;
|
||||
};
|
||||
@@ -794,6 +800,7 @@ OlmDevice.prototype.decryptMessage = async function(
|
||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[decryptMessage]"),
|
||||
);
|
||||
return payloadString;
|
||||
};
|
||||
@@ -825,6 +832,7 @@ OlmDevice.prototype.matchesSession = async function(
|
||||
matches = sessionInfo.session.matches_inbound(ciphertext);
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[matchesSession]"),
|
||||
);
|
||||
return matches;
|
||||
};
|
||||
@@ -841,7 +849,6 @@ OlmDevice.prototype.filterOutNotifiedErrorDevices = async function(devices) {
|
||||
return await this._cryptoStore.filterOutNotifiedErrorDevices(devices);
|
||||
};
|
||||
|
||||
|
||||
// Outbound group session
|
||||
// ======================
|
||||
|
||||
@@ -856,7 +863,6 @@ OlmDevice.prototype._saveOutboundGroupSession = function(session) {
|
||||
this._outboundGroupSessionStore[session.session_id()] = pickledSession;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* extract an OutboundGroupSession from _outboundGroupSessionStore and call the
|
||||
* given function
|
||||
@@ -881,7 +887,6 @@ OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Generate a new outbound group session
|
||||
*
|
||||
@@ -898,7 +903,6 @@ OlmDevice.prototype.createOutboundGroupSession = function() {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt an outgoing message with an outbound group session
|
||||
*
|
||||
@@ -938,7 +942,6 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Inbound group session
|
||||
// =====================
|
||||
|
||||
@@ -1033,6 +1036,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
'readwrite', [
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
||||
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
|
||||
], (txn) => {
|
||||
/* if we already have this session, consider updating it */
|
||||
this._getInboundGroupSession(
|
||||
@@ -1059,9 +1063,14 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
+ senderKey + "/" + sessionId,
|
||||
);
|
||||
if (existingSession.first_known_index()
|
||||
<= session.first_known_index()) {
|
||||
<= session.first_known_index()
|
||||
&& !(existingSession.first_known_index() == session.first_known_index()
|
||||
&& !extraSessionData.untrusted
|
||||
&& existingSessionData.untrusted)) {
|
||||
// existing session has lower index (i.e. can
|
||||
// decrypt more), so keep it
|
||||
// decrypt more), or they have the same index and
|
||||
// the new sessions trust does not win over the old
|
||||
// sessions trust, so keep it
|
||||
logger.log(
|
||||
`Keeping existing megolm session ${sessionId}`,
|
||||
);
|
||||
@@ -1084,12 +1093,19 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
this._cryptoStore.storeEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, sessionData, txn,
|
||||
);
|
||||
|
||||
if (!existingSession && extraSessionData.sharedHistory) {
|
||||
this._cryptoStore.addSharedHistoryInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
logger.withPrefix("[addInboundGroupSession]"),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1260,6 +1276,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
},
|
||||
);
|
||||
},
|
||||
logger.withPrefix("[decryptGroupMessage]"),
|
||||
);
|
||||
|
||||
if (error) {
|
||||
@@ -1305,6 +1322,7 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
|
||||
},
|
||||
);
|
||||
},
|
||||
logger.withPrefix("[hasInboundSessionKeys]"),
|
||||
);
|
||||
|
||||
return result;
|
||||
@@ -1360,10 +1378,12 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
||||
"forwarding_curve25519_key_chain":
|
||||
sessionData.forwardingCurve25519KeyChain || [],
|
||||
"sender_claimed_ed25519_key": senderEd25519Key,
|
||||
"shared_history": sessionData.sharedHistory || false,
|
||||
};
|
||||
},
|
||||
);
|
||||
},
|
||||
logger.withPrefix("[getInboundGroupSessionKey]"),
|
||||
);
|
||||
|
||||
return result;
|
||||
@@ -1391,10 +1411,24 @@ OlmDevice.prototype.exportInboundGroupSession = function(
|
||||
"session_key": session.export_session(messageIndex),
|
||||
"forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [],
|
||||
"first_known_index": session.first_known_index(),
|
||||
"org.matrix.msc3061.shared_history": sessionData.sharedHistory || false,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
OlmDevice.prototype.getSharedHistoryInboundGroupSessions = async function(roomId) {
|
||||
let result;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [
|
||||
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
|
||||
], (txn) => {
|
||||
result = this._cryptoStore.getSharedHistoryInboundGroupSessions(roomId, txn);
|
||||
},
|
||||
logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"),
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
// Utilities
|
||||
// =========
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from '../logger';
|
||||
import * as utils from '../utils';
|
||||
import { logger } from '../logger';
|
||||
|
||||
/**
|
||||
* Internal module. Management of outgoing room key requests.
|
||||
@@ -496,7 +495,7 @@ function stringifyRequestBody(requestBody) {
|
||||
|
||||
function stringifyRecipientList(recipients) {
|
||||
return '['
|
||||
+ utils.map(recipients, (r) => `${r.userId}:${r.deviceId}`).join(",")
|
||||
+ recipients.map((r) => `${r.userId}:${r.deviceId}`).join(",")
|
||||
+ ']';
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ limitations under the License.
|
||||
* Manages the list of encrypted rooms
|
||||
*/
|
||||
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
|
||||
/**
|
||||
* @alias module:crypto/RoomList
|
||||
|
||||
+34
-29
@@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
import {logger} from '../logger';
|
||||
import { EventEmitter } from 'events';
|
||||
import { logger } from '../logger';
|
||||
import * as olmlib from './olmlib';
|
||||
import {randomString} from '../randomstring';
|
||||
import {encryptAES, decryptAES} from './aes';
|
||||
import {encodeBase64} from "./olmlib";
|
||||
import { randomString } from '../randomstring';
|
||||
import { encryptAES, decryptAES } from './aes';
|
||||
import { encodeBase64 } from "./olmlib";
|
||||
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_AES
|
||||
= "m.secret_storage.v1.aes-hmac-sha2";
|
||||
@@ -81,25 +81,27 @@ export class SecretStorage extends EventEmitter {
|
||||
* @param {string} [keyId] the ID of the key. If not given, a random
|
||||
* ID will be generated.
|
||||
*
|
||||
* @return {string} the ID of the key
|
||||
* @return {object} An object with:
|
||||
* keyId: {string} the ID of the key
|
||||
* keyInfo: {object} details about the key (iv, mac, passphrase)
|
||||
*/
|
||||
async addKey(algorithm, opts, keyId) {
|
||||
const keyData = {algorithm};
|
||||
const keyInfo = { algorithm };
|
||||
|
||||
if (!opts) opts = {};
|
||||
|
||||
if (opts.name) {
|
||||
keyData.name = opts.name;
|
||||
keyInfo.name = opts.name;
|
||||
}
|
||||
|
||||
if (algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
if (opts.passphrase) {
|
||||
keyData.passphrase = opts.passphrase;
|
||||
keyInfo.passphrase = opts.passphrase;
|
||||
}
|
||||
if (opts.key) {
|
||||
const {iv, mac} = await SecretStorage._calculateKeyCheck(opts.key);
|
||||
keyData.iv = iv;
|
||||
keyData.mac = mac;
|
||||
const { iv, mac } = await SecretStorage._calculateKeyCheck(opts.key);
|
||||
keyInfo.iv = iv;
|
||||
keyInfo.mac = mac;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown key algorithm ${opts.algorithm}`);
|
||||
@@ -116,10 +118,13 @@ export class SecretStorage extends EventEmitter {
|
||||
}
|
||||
|
||||
await this._baseApis.setAccountData(
|
||||
`m.secret_storage.key.${keyId}`, keyData,
|
||||
`m.secret_storage.key.${keyId}`, keyInfo,
|
||||
);
|
||||
|
||||
return keyId;
|
||||
return {
|
||||
keyId,
|
||||
keyInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +171,7 @@ export class SecretStorage extends EventEmitter {
|
||||
async checkKey(key, info) {
|
||||
if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
if (info.mac) {
|
||||
const {mac} = await SecretStorage._calculateKeyCheck(key, info.iv);
|
||||
const { mac } = await SecretStorage._calculateKeyCheck(key, info.iv);
|
||||
return info.mac.replace(/=+$/g, '') === mac.replace(/=+$/g, '');
|
||||
} else {
|
||||
// if we have no information, we have to assume the key is right
|
||||
@@ -215,7 +220,7 @@ export class SecretStorage extends EventEmitter {
|
||||
|
||||
// encrypt secret, based on the algorithm
|
||||
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
const keys = {[keyId]: keyInfo};
|
||||
const keys = { [keyId]: keyInfo };
|
||||
const [, encryption] = await this._getSecretStorageKey(keys, name);
|
||||
encrypted[keyId] = await encryption.encrypt(secret);
|
||||
} else {
|
||||
@@ -226,7 +231,7 @@ export class SecretStorage extends EventEmitter {
|
||||
}
|
||||
|
||||
// save encrypted secret
|
||||
await this._baseApis.setAccountData(name, {encrypted});
|
||||
await this._baseApis.setAccountData(name, { encrypted });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -439,7 +444,7 @@ export class SecretStorage extends EventEmitter {
|
||||
&& this._incomingRequests[deviceId][content.request_id]) {
|
||||
logger.info("received request cancellation for secret (" + sender
|
||||
+ ", " + deviceId + ", " + content.request_id + ")");
|
||||
this.baseApis.emit("crypto.secrets.requestCancelled", {
|
||||
this._baseApis.emit("crypto.secrets.requestCancelled", {
|
||||
user_id: sender,
|
||||
device_id: deviceId,
|
||||
request_id: content.request_id,
|
||||
@@ -457,13 +462,13 @@ export class SecretStorage extends EventEmitter {
|
||||
if (!this._cryptoCallbacks.onSecretRequested) {
|
||||
return;
|
||||
}
|
||||
const secret = await this._cryptoCallbacks.onSecretRequested({
|
||||
user_id: sender,
|
||||
device_id: deviceId,
|
||||
request_id: content.request_id,
|
||||
name: content.name,
|
||||
device_trust: this._baseApis.checkDeviceTrust(sender, deviceId),
|
||||
});
|
||||
const secret = await this._cryptoCallbacks.onSecretRequested(
|
||||
sender,
|
||||
deviceId,
|
||||
content.request_id,
|
||||
content.name,
|
||||
this._baseApis.checkDeviceTrust(sender, deviceId),
|
||||
);
|
||||
if (secret) {
|
||||
logger.info(`Preparing ${content.name} secret for ${deviceId}`);
|
||||
const payload = {
|
||||
@@ -475,11 +480,11 @@ export class SecretStorage extends EventEmitter {
|
||||
};
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key,
|
||||
sender_key: this._baseApis.crypto._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this._baseApis._crypto._olmDevice,
|
||||
this._baseApis.crypto._olmDevice,
|
||||
this._baseApis,
|
||||
{
|
||||
[sender]: [
|
||||
@@ -491,7 +496,7 @@ export class SecretStorage extends EventEmitter {
|
||||
encryptedContent.ciphertext,
|
||||
this._baseApis.getUserId(),
|
||||
this._baseApis.deviceId,
|
||||
this._baseApis._crypto._olmDevice,
|
||||
this._baseApis.crypto._olmDevice,
|
||||
sender,
|
||||
this._baseApis.getStoredDevice(sender, deviceId),
|
||||
payload,
|
||||
@@ -522,7 +527,7 @@ export class SecretStorage extends EventEmitter {
|
||||
if (requestControl) {
|
||||
// make sure that the device that sent it is one of the devices that
|
||||
// we requested from
|
||||
const deviceInfo = this._baseApis._crypto._deviceList.getDeviceByIdentityKey(
|
||||
const deviceInfo = this._baseApis.crypto._deviceList.getDeviceByIdentityKey(
|
||||
olmlib.OLM_ALGORITHM,
|
||||
event.getSenderKey(),
|
||||
);
|
||||
|
||||
+7
-7
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {getCrypto} from '../utils';
|
||||
import {decodeBase64, encodeBase64} from './olmlib';
|
||||
import { getCrypto } from '../utils';
|
||||
import { decodeBase64, encodeBase64 } from './olmlib';
|
||||
|
||||
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
|
||||
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
|
||||
@@ -148,7 +148,7 @@ async function encryptBrowser(data, key, name, ivStr) {
|
||||
);
|
||||
|
||||
const hmac = await subtleCrypto.sign(
|
||||
{name: 'HMAC'},
|
||||
{ name: 'HMAC' },
|
||||
hmacKey,
|
||||
ciphertext,
|
||||
);
|
||||
@@ -176,7 +176,7 @@ async function decryptBrowser(data, key, name) {
|
||||
const ciphertext = decodeBase64(data.ciphertext);
|
||||
|
||||
if (!await subtleCrypto.verify(
|
||||
{name: "HMAC"},
|
||||
{ name: "HMAC" },
|
||||
hmacKey,
|
||||
decodeBase64(data.mac),
|
||||
ciphertext,
|
||||
@@ -201,7 +201,7 @@ async function deriveKeysBrowser(key, name) {
|
||||
const hkdfkey = await subtleCrypto.importKey(
|
||||
'raw',
|
||||
key,
|
||||
{name: "HKDF"},
|
||||
{ name: "HKDF" },
|
||||
false,
|
||||
["deriveBits"],
|
||||
);
|
||||
@@ -222,7 +222,7 @@ async function deriveKeysBrowser(key, name) {
|
||||
const aesProm = subtleCrypto.importKey(
|
||||
'raw',
|
||||
aesKey,
|
||||
{name: 'AES-CTR'},
|
||||
{ name: 'AES-CTR' },
|
||||
false,
|
||||
['encrypt', 'decrypt'],
|
||||
);
|
||||
@@ -232,7 +232,7 @@ async function deriveKeysBrowser(key, name) {
|
||||
hmacKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: {name: 'SHA-256'},
|
||||
hash: { name: 'SHA-256' },
|
||||
},
|
||||
false,
|
||||
['sign', 'verify'],
|
||||
|
||||
@@ -46,7 +46,7 @@ export const DECRYPTION_CLASSES = {};
|
||||
* @param {string} params.deviceId The identifier for this device.
|
||||
* @param {module:crypto} params.crypto crypto core
|
||||
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
||||
* @param {MatrixClient} baseApis base matrix api interface
|
||||
* @param {string} params.roomId The ID of the room we will be sending to
|
||||
* @param {object} params.config The body of the m.room.encryption event
|
||||
*/
|
||||
@@ -102,7 +102,7 @@ export class EncryptionAlgorithm {
|
||||
* @param {string} params.userId The UserID for the local user
|
||||
* @param {module:crypto} params.crypto crypto core
|
||||
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
||||
* @param {MatrixClient} baseApis base matrix api interface
|
||||
* @param {string=} params.roomId The ID of the room we will be receiving
|
||||
* from. Null for to-device events.
|
||||
*/
|
||||
|
||||
+236
-94
@@ -22,9 +22,9 @@ limitations under the License.
|
||||
* @module crypto/algorithms/megolm
|
||||
*/
|
||||
|
||||
import {logger} from '../../logger';
|
||||
import { logger } from '../../logger';
|
||||
import * as utils from "../../utils";
|
||||
import {polyfillSuper} from "../../utils";
|
||||
import { polyfillSuper } from "../../utils";
|
||||
import * as olmlib from "../olmlib";
|
||||
import {
|
||||
DecryptionAlgorithm,
|
||||
@@ -34,13 +34,29 @@ import {
|
||||
UnknownDeviceError,
|
||||
} from "./base";
|
||||
|
||||
import {WITHHELD_MESSAGES} from '../OlmDevice';
|
||||
import { WITHHELD_MESSAGES } from '../OlmDevice';
|
||||
|
||||
// determine whether the key can be shared with invitees
|
||||
function isRoomSharedHistory(room) {
|
||||
const visibilityEvent = room.currentState &&
|
||||
room.currentState.getStateEvents("m.room.history_visibility", "");
|
||||
// NOTE: if the room visibility is unset, it would normally default to
|
||||
// "world_readable".
|
||||
// (https://spec.matrix.org/unstable/client-server-api/#server-behaviour-5)
|
||||
// But we will be paranoid here, and treat it as a situation where the room
|
||||
// is not shared-history
|
||||
const visibility = visibilityEvent && visibilityEvent.getContent() &&
|
||||
visibilityEvent.getContent().history_visibility;
|
||||
return ["world_readable", "shared"].includes(visibility);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} sessionId
|
||||
* @param {boolean} sharedHistory whether the session can be freely shared with
|
||||
* other group members, according to the room history visibility settings
|
||||
*
|
||||
* @property {string} sessionId
|
||||
* @property {Number} useCount number of times this session has been used
|
||||
@@ -50,15 +66,15 @@ import {WITHHELD_MESSAGES} from '../OlmDevice';
|
||||
* devices with which we have shared the session key
|
||||
* userId -> {deviceId -> msgindex}
|
||||
*/
|
||||
function OutboundSessionInfo(sessionId) {
|
||||
function OutboundSessionInfo(sessionId, sharedHistory = false) {
|
||||
this.sessionId = sessionId;
|
||||
this.useCount = 0;
|
||||
this.creationTime = new Date().getTime();
|
||||
this.sharedWithDevices = {};
|
||||
this.blockedDevicesNotified = {};
|
||||
this.sharedHistory = sharedHistory;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if it's time to rotate the session
|
||||
*
|
||||
@@ -141,7 +157,6 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Megolm encryption implementation
|
||||
*
|
||||
@@ -183,6 +198,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
* @param {Object} devicesInRoom The devices in this room, indexed by user ID
|
||||
* @param {Object} blocked The devices that are blocked, indexed by user ID
|
||||
* @param {boolean} [singleOlmCreationPhase] Only perform one round of olm
|
||||
@@ -192,7 +208,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
|
||||
* OutboundSessionInfo when setup is complete.
|
||||
*/
|
||||
MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
devicesInRoom, blocked, singleOlmCreationPhase,
|
||||
room, devicesInRoom, blocked, singleOlmCreationPhase,
|
||||
) {
|
||||
let session;
|
||||
|
||||
@@ -204,6 +220,13 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
const prepareSession = async (oldSession) => {
|
||||
session = oldSession;
|
||||
|
||||
const sharedHistory = isRoomSharedHistory(room);
|
||||
|
||||
// history visibility changed
|
||||
if (session && sharedHistory !== session.sharedHistory) {
|
||||
session = null;
|
||||
}
|
||||
|
||||
// need to make a brand new session?
|
||||
if (session && session.needsRotation(this._sessionRotationPeriodMsgs,
|
||||
this._sessionRotationPeriodMs)
|
||||
@@ -219,7 +242,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
|
||||
if (!session) {
|
||||
logger.log(`Starting new megolm session for room ${this._roomId}`);
|
||||
session = await this._prepareNewSession();
|
||||
session = await this._prepareNewSession(sharedHistory);
|
||||
logger.log(`Started new megolm session ${session.sessionId} ` +
|
||||
`for room ${this._roomId}`);
|
||||
this._outboundSessions[session.sessionId] = session;
|
||||
@@ -250,11 +273,12 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
const payload = {
|
||||
type: "m.room_key",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: this._roomId,
|
||||
session_id: session.sessionId,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": this._roomId,
|
||||
"session_id": session.sessionId,
|
||||
"session_key": key.key,
|
||||
"chain_index": key.chain_index,
|
||||
"org.matrix.msc3061.shared_history": sharedHistory,
|
||||
},
|
||||
};
|
||||
const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(
|
||||
@@ -264,11 +288,14 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
// share keys with devices that we already have a session for
|
||||
logger.debug(`Sharing keys with existing Olm sessions in ${this._roomId}`);
|
||||
await this._shareKeyWithOlmSessions(
|
||||
session, key, payload, olmSessions,
|
||||
);
|
||||
logger.debug(`Shared keys with existing Olm sessions in ${this._roomId}`);
|
||||
})(),
|
||||
(async () => {
|
||||
logger.debug(`Sharing keys (start phase 1) with new Olm sessions in ${this._roomId}`);
|
||||
const errorDevices = [];
|
||||
|
||||
// meanwhile, establish olm sessions for devices that we don't
|
||||
@@ -282,6 +309,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
session, key, payload, devicesWithoutSession, errorDevices,
|
||||
singleOlmCreationPhase ? 10000 : 2000, failedServers,
|
||||
);
|
||||
logger.debug(`Shared keys (end phase 1) with new Olm sessions in ${this._roomId}`);
|
||||
|
||||
if (!singleOlmCreationPhase && (Date.now() - start < 10000)) {
|
||||
// perform the second phase of olm session creation if requested,
|
||||
@@ -298,7 +326,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
failedServerMap.add(server);
|
||||
}
|
||||
const failedDevices = [];
|
||||
for (const {userId, deviceInfo} of errorDevices) {
|
||||
for (const { userId, deviceInfo } of errorDevices) {
|
||||
const userHS = userId.slice(userId.indexOf(":") + 1);
|
||||
if (failedServerMap.has(userHS)) {
|
||||
retryDevices[userId] = retryDevices[userId] || [];
|
||||
@@ -306,23 +334,28 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
} else {
|
||||
// if we aren't going to retry, then handle it
|
||||
// as a failed device
|
||||
failedDevices.push({userId, deviceInfo});
|
||||
failedDevices.push({ userId, deviceInfo });
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`Sharing keys (start phase 2) with new Olm sessions in ${this._roomId}`);
|
||||
await this._shareKeyWithDevices(
|
||||
session, key, payload, retryDevices, failedDevices, 30000,
|
||||
);
|
||||
logger.debug(`Shared keys (end phase 2) with new Olm sessions in ${this._roomId}`);
|
||||
|
||||
await this._notifyFailedOlmDevices(session, key, failedDevices);
|
||||
})();
|
||||
} else {
|
||||
await this._notifyFailedOlmDevices(session, key, errorDevices);
|
||||
}
|
||||
logger.debug(`Shared keys (all phases done) with new Olm sessions in ${this._roomId}`);
|
||||
})(),
|
||||
(async () => {
|
||||
logger.debug(`Notifying blocked devices in ${this._roomId}`);
|
||||
// also, notify blocked devices that they're blocked
|
||||
const blockedMap = {};
|
||||
let blockedCount = 0;
|
||||
for (const [userId, userBlockedDevices] of Object.entries(blocked)) {
|
||||
for (const [deviceId, device] of Object.entries(userBlockedDevices)) {
|
||||
if (
|
||||
@@ -330,12 +363,14 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
session.blockedDevicesNotified[userId][deviceId] === undefined
|
||||
) {
|
||||
blockedMap[userId] = blockedMap[userId] || {};
|
||||
blockedMap[userId][deviceId] = {device};
|
||||
blockedMap[userId][deviceId] = { device };
|
||||
blockedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this._notifyBlockedDevices(session, blockedMap);
|
||||
logger.debug(`Notified ${blockedCount} blocked devices in ${this._roomId}`);
|
||||
})(),
|
||||
]);
|
||||
};
|
||||
@@ -348,6 +383,11 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
// first wait for the previous share to complete
|
||||
const prom = this._setupPromise.then(prepareSession);
|
||||
|
||||
// Ensure any failures are logged for debugging
|
||||
prom.catch(e => {
|
||||
logger.error(`Failed to ensure outbound session in ${this._roomId}`, e);
|
||||
});
|
||||
|
||||
// _setupPromise resolves to `session` whether or not the share succeeds
|
||||
this._setupPromise = prom.then(returnSession, returnSession);
|
||||
|
||||
@@ -358,30 +398,26 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {boolean} sharedHistory
|
||||
*
|
||||
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||
*/
|
||||
MegolmEncryption.prototype._prepareNewSession = async function() {
|
||||
MegolmEncryption.prototype._prepareNewSession = async function(sharedHistory) {
|
||||
const sessionId = this._olmDevice.createOutboundGroupSession();
|
||||
const key = this._olmDevice.getOutboundGroupSessionKey(sessionId);
|
||||
|
||||
await this._olmDevice.addInboundGroupSession(
|
||||
this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId,
|
||||
key.key, {ed25519: this._olmDevice.deviceEd25519Key},
|
||||
key.key, { ed25519: this._olmDevice.deviceEd25519Key }, false,
|
||||
{ sharedHistory: sharedHistory },
|
||||
);
|
||||
|
||||
if (this._crypto.backupInfo) {
|
||||
// don't wait for it to complete
|
||||
this._crypto.backupGroupSession(
|
||||
this._roomId, this._olmDevice.deviceCurve25519Key, [],
|
||||
sessionId, key.key,
|
||||
).catch((e) => {
|
||||
// This throws if the upload failed, but this is fine
|
||||
// since it will have written it to the db and will retry.
|
||||
logger.log("Failed to back up megolm session", e);
|
||||
});
|
||||
}
|
||||
// don't wait for it to complete
|
||||
this._crypto._backupManager.backupGroupSession(
|
||||
this._olmDevice.deviceCurve25519Key, sessionId,
|
||||
);
|
||||
|
||||
return new OutboundSessionInfo(sessionId);
|
||||
return new OutboundSessionInfo(sessionId, sharedHistory);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -415,7 +451,7 @@ MegolmEncryption.prototype._getDevicesWithoutSessions = function(
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
|
||||
noOlmDevices.push({userId, deviceInfo});
|
||||
noOlmDevices.push({ userId, deviceInfo });
|
||||
delete sessionResults[deviceId];
|
||||
|
||||
// ensureOlmSessionsForUsers has already done the logging,
|
||||
@@ -662,14 +698,15 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
|
||||
const payload = {
|
||||
type: "m.forwarded_room_key",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: this._roomId,
|
||||
session_id: sessionId,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
sender_key: senderKey,
|
||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": this._roomId,
|
||||
"session_id": sessionId,
|
||||
"session_key": key.key,
|
||||
"chain_index": key.chain_index,
|
||||
"sender_key": senderKey,
|
||||
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
|
||||
"forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
|
||||
"org.matrix.msc3061.shared_history": key.shared_history || false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -723,13 +760,18 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
|
||||
MegolmEncryption.prototype._shareKeyWithDevices = async function(
|
||||
session, key, payload, devicesByUser, errorDevices, otkTimeout, failedServers,
|
||||
) {
|
||||
logger.debug(`Ensuring Olm sessions for devices in ${this._roomId}`);
|
||||
const devicemap = await olmlib.ensureOlmSessionsForDevices(
|
||||
this._olmDevice, this._baseApis, devicesByUser, otkTimeout, failedServers,
|
||||
logger.withPrefix(`[${this._roomId}]`),
|
||||
);
|
||||
logger.debug(`Ensured Olm sessions for devices in ${this._roomId}`);
|
||||
|
||||
this._getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices);
|
||||
|
||||
logger.debug(`Sharing keys with Olm sessions in ${this._roomId}`);
|
||||
await this._shareKeyWithOlmSessions(session, key, payload, devicemap);
|
||||
logger.debug(`Shared keys with Olm sessions in ${this._roomId}`);
|
||||
};
|
||||
|
||||
MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
|
||||
@@ -738,16 +780,17 @@ MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
|
||||
const userDeviceMaps = this._splitDevices(devicemap);
|
||||
|
||||
for (let i = 0; i < userDeviceMaps.length; i++) {
|
||||
const taskDetail =
|
||||
`megolm keys for ${session.sessionId} ` +
|
||||
`in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`;
|
||||
try {
|
||||
logger.debug(`Sharing ${taskDetail}`);
|
||||
await this._encryptAndSendKeysToDevices(
|
||||
session, key.chain_index, userDeviceMaps[i], payload,
|
||||
);
|
||||
logger.log(`Completed megolm keyshare for ${session.sessionId} `
|
||||
+ `in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`);
|
||||
logger.debug(`Shared ${taskDetail}`);
|
||||
} catch (e) {
|
||||
logger.log(`megolm keyshare for ${session.sessionId} in ${this._roomId} `
|
||||
+ `(slice ${i + 1}/${userDeviceMaps.length}) failed`);
|
||||
|
||||
logger.error(`Failed to share ${taskDetail}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -766,9 +809,14 @@ MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
|
||||
MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
|
||||
session, key, failedDevices,
|
||||
) {
|
||||
logger.debug(
|
||||
`Notifying ${failedDevices.length} devices we failed to ` +
|
||||
`create Olm sessions in ${this._roomId}`,
|
||||
);
|
||||
|
||||
// mark the devices that failed as "handled" because we don't want to try
|
||||
// to claim a one-time-key for dead devices on every message.
|
||||
for (const {userId, deviceInfo} of failedDevices) {
|
||||
for (const { userId, deviceInfo } of failedDevices) {
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
|
||||
session.markSharedWithDevice(
|
||||
@@ -780,8 +828,12 @@ MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
|
||||
await this._olmDevice.filterOutNotifiedErrorDevices(
|
||||
failedDevices,
|
||||
);
|
||||
logger.debug(
|
||||
`Filtered down to ${filteredFailedDevices.length} error devices ` +
|
||||
`in ${this._roomId}`,
|
||||
);
|
||||
const blockedMap = {};
|
||||
for (const {userId, deviceInfo} of filteredFailedDevices) {
|
||||
for (const { userId, deviceInfo } of filteredFailedDevices) {
|
||||
blockedMap[userId] = blockedMap[userId] || {};
|
||||
// we use a similar format to what
|
||||
// olmlib.ensureOlmSessionsForDevices returns, so that
|
||||
@@ -797,6 +849,10 @@ MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
|
||||
|
||||
// send the notifications
|
||||
await this._notifyBlockedDevices(session, blockedMap);
|
||||
logger.debug(
|
||||
`Notified ${filteredFailedDevices.length} devices we failed to ` +
|
||||
`create Olm sessions in ${this._roomId}`,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -846,24 +902,41 @@ MegolmEncryption.prototype.prepareToEncrypt = function(room) {
|
||||
// We're already preparing something, so don't do anything else.
|
||||
// FIXME: check if we need to restart
|
||||
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
|
||||
const elapsedTime = Date.now() - this.encryptionPreparationMetadata.startTime;
|
||||
logger.debug(
|
||||
`Already started preparing to encrypt for ${this._roomId} ` +
|
||||
`${elapsedTime} ms ago, skipping`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Preparing to encrypt events for ${this._roomId}`);
|
||||
|
||||
this.encryptionPreparationMetadata = {
|
||||
startTime: Date.now(),
|
||||
};
|
||||
this.encryptionPreparation = (async () => {
|
||||
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
|
||||
try {
|
||||
logger.debug(`Getting devices in ${this._roomId}`);
|
||||
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
|
||||
|
||||
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
|
||||
// Drop unknown devices for now. When the message gets sent, we'll
|
||||
// throw an error, but we'll still be prepared to send to the known
|
||||
// devices.
|
||||
this._removeUnknownDevices(devicesInRoom);
|
||||
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
|
||||
// Drop unknown devices for now. When the message gets sent, we'll
|
||||
// throw an error, but we'll still be prepared to send to the known
|
||||
// devices.
|
||||
this._removeUnknownDevices(devicesInRoom);
|
||||
}
|
||||
|
||||
logger.debug(`Ensuring outbound session in ${this._roomId}`);
|
||||
await this._ensureOutboundSession(room, devicesInRoom, blocked, true);
|
||||
|
||||
logger.debug(`Ready to encrypt events for ${this._roomId}`);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to prepare to encrypt events for ${this._roomId}`, e);
|
||||
} finally {
|
||||
delete this.encryptionPreparationMetadata;
|
||||
delete this.encryptionPreparation;
|
||||
}
|
||||
|
||||
await this._ensureOutboundSession(devicesInRoom, blocked, true);
|
||||
|
||||
delete this.encryptionPreparation;
|
||||
})();
|
||||
};
|
||||
|
||||
@@ -899,7 +972,7 @@ MegolmEncryption.prototype.encryptMessage = async function(room, eventType, cont
|
||||
this._checkForUnknownDevices(devicesInRoom);
|
||||
}
|
||||
|
||||
const session = await this._ensureOutboundSession(devicesInRoom, blocked);
|
||||
const session = await this._ensureOutboundSession(room, devicesInRoom, blocked);
|
||||
const payloadJson = {
|
||||
room_id: this._roomId,
|
||||
type: eventType,
|
||||
@@ -1000,7 +1073,7 @@ MegolmEncryption.prototype._removeUnknownDevices = function(devicesInRoom) {
|
||||
*/
|
||||
MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
|
||||
const members = await room.getEncryptionTargetMembers();
|
||||
const roomMembers = utils.map(members, function(u) {
|
||||
const roomMembers = members.map(function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
@@ -1265,7 +1338,6 @@ MegolmDecryption.prototype._removeEventFromPendingList = function(event) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
@@ -1295,7 +1367,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
if (event.getType() == "m.forwarded_room_key") {
|
||||
exportFormat = true;
|
||||
forwardingKeyChain = content.forwarding_curve25519_key_chain;
|
||||
if (!utils.isArray(forwardingKeyChain)) {
|
||||
if (!Array.isArray(forwardingKeyChain)) {
|
||||
forwardingKeyChain = [];
|
||||
}
|
||||
|
||||
@@ -1324,10 +1396,14 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
keysClaimed = event.getKeysClaimed();
|
||||
}
|
||||
|
||||
const extraSessionData = {};
|
||||
if (content["org.matrix.msc3061.shared_history"]) {
|
||||
extraSessionData.sharedHistory = true;
|
||||
}
|
||||
return this._olmDevice.addInboundGroupSession(
|
||||
content.room_id, senderKey, forwardingKeyChain, sessionId,
|
||||
content.session_key, keysClaimed,
|
||||
exportFormat,
|
||||
exportFormat, extraSessionData,
|
||||
).then(() => {
|
||||
// have another go at decrypting events sent with this session.
|
||||
this._retryDecryption(senderKey, sessionId)
|
||||
@@ -1347,18 +1423,8 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
if (this._crypto.backupInfo) {
|
||||
// don't wait for the keys to be backed up for the server
|
||||
this._crypto.backupGroupSession(
|
||||
content.room_id, senderKey, forwardingKeyChain,
|
||||
content.session_id, content.session_key, keysClaimed,
|
||||
exportFormat,
|
||||
).catch((e) => {
|
||||
// This throws if the upload failed, but this is fine
|
||||
// since it will have written it to the db and will retry.
|
||||
logger.log("Failed to back up megolm session", e);
|
||||
});
|
||||
}
|
||||
// don't wait for the keys to be backed up for the server
|
||||
this._crypto._backupManager.backupGroupSession(senderKey, content.session_id);
|
||||
}).catch((e) => {
|
||||
logger.error(`Error handling m.room_key_event: ${e}`);
|
||||
});
|
||||
@@ -1415,7 +1481,7 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
|
||||
}
|
||||
}
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this._olmDevice, this._baseApis, {[sender]: [device]}, false,
|
||||
this._olmDevice, this._baseApis, { [sender]: [device] }, false,
|
||||
);
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
@@ -1429,7 +1495,7 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
|
||||
this._olmDevice,
|
||||
sender,
|
||||
device,
|
||||
{type: "m.dummy"},
|
||||
{ type: "m.dummy" },
|
||||
);
|
||||
|
||||
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true);
|
||||
@@ -1533,14 +1599,15 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
|
||||
return {
|
||||
type: "m.forwarded_room_key",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: senderKey,
|
||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||
session_id: sessionId,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": roomId,
|
||||
"sender_key": senderKey,
|
||||
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
|
||||
"session_id": sessionId,
|
||||
"session_key": key.key,
|
||||
"chain_index": key.chain_index,
|
||||
"forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
|
||||
"org.matrix.msc3061.shared_history": key.shared_history || false,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1554,6 +1621,13 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
|
||||
* @param {string} [opts.source] where the key came from
|
||||
*/
|
||||
MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
|
||||
const extraSessionData = {};
|
||||
if (opts.untrusted) {
|
||||
extraSessionData.untrusted = true;
|
||||
}
|
||||
if (session["org.matrix.msc3061.shared_history"]) {
|
||||
extraSessionData.sharedHistory = true;
|
||||
}
|
||||
return this._olmDevice.addInboundGroupSession(
|
||||
session.room_id,
|
||||
session.sender_key,
|
||||
@@ -1562,18 +1636,12 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
|
||||
session.session_key,
|
||||
session.sender_claimed_keys,
|
||||
true,
|
||||
opts.untrusted ? { untrusted: opts.untrusted } : {},
|
||||
extraSessionData,
|
||||
).then(() => {
|
||||
if (this._crypto.backupInfo && opts.source !== "backup") {
|
||||
if (opts.source !== "backup") {
|
||||
// don't wait for it to complete
|
||||
this._crypto.backupGroupSession(
|
||||
session.room_id,
|
||||
session.sender_key,
|
||||
session.forwarding_curve25519_key_chain,
|
||||
session.session_id,
|
||||
session.session_key,
|
||||
session.sender_claimed_keys,
|
||||
true,
|
||||
this._crypto._backupManager.backupGroupSession(
|
||||
session.sender_key, session.session_id,
|
||||
).catch((e) => {
|
||||
// This throws if the upload failed, but this is fine
|
||||
// since it will have written it to the db and will retry.
|
||||
@@ -1610,7 +1678,7 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI
|
||||
|
||||
await Promise.all([...pending].map(async (ev) => {
|
||||
try {
|
||||
await ev.attemptDecryption(this._crypto, true);
|
||||
await ev.attemptDecryption(this._crypto, { isRetry: true });
|
||||
} catch (e) {
|
||||
// don't die if something goes wrong
|
||||
}
|
||||
@@ -1641,6 +1709,80 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey)
|
||||
return !this._pendingEvents[senderKey];
|
||||
};
|
||||
|
||||
MegolmDecryption.prototype.sendSharedHistoryInboundSessions = async function(devicesByUser) {
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this._olmDevice, this._baseApis, devicesByUser,
|
||||
);
|
||||
|
||||
logger.log("sendSharedHistoryInboundSessions to users", Object.keys(devicesByUser));
|
||||
|
||||
const sharedHistorySessions =
|
||||
await this._olmDevice.getSharedHistoryInboundGroupSessions(
|
||||
this._roomId,
|
||||
);
|
||||
logger.log("shared-history sessions", sharedHistorySessions);
|
||||
for (const [senderKey, sessionId] of sharedHistorySessions) {
|
||||
const payload = await this._buildKeyForwardingMessage(
|
||||
this._roomId, senderKey, sessionId,
|
||||
);
|
||||
|
||||
const promises = [];
|
||||
const contentMap = {};
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
contentMap[userId] = {};
|
||||
for (const deviceInfo of devices) {
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
contentMap[userId][deviceInfo.deviceId] = encryptedContent;
|
||||
promises.push(
|
||||
olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
this._userId,
|
||||
this._deviceId,
|
||||
this._olmDevice,
|
||||
userId,
|
||||
deviceInfo,
|
||||
payload,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
// prune out any devices that encryptMessageForDevice could not encrypt for,
|
||||
// in which case it will have just not added anything to the ciphertext object.
|
||||
// There's no point sending messages to devices if we couldn't encrypt to them,
|
||||
// since that's effectively a blank message.
|
||||
for (const userId of Object.keys(contentMap)) {
|
||||
for (const deviceId of Object.keys(contentMap[userId])) {
|
||||
if (Object.keys(contentMap[userId][deviceId].ciphertext).length === 0) {
|
||||
logger.log(
|
||||
"No ciphertext for device " +
|
||||
userId + ":" + deviceId + ": pruning",
|
||||
);
|
||||
delete contentMap[userId][deviceId];
|
||||
}
|
||||
}
|
||||
// No devices left for that user? Strip that too.
|
||||
if (Object.keys(contentMap[userId]).length === 0) {
|
||||
logger.log("Pruned all devices for user " + userId);
|
||||
delete contentMap[userId];
|
||||
}
|
||||
}
|
||||
|
||||
// Is there anything left?
|
||||
if (Object.keys(contentMap).length === 0) {
|
||||
logger.log("No users left to send to: aborting");
|
||||
return;
|
||||
}
|
||||
|
||||
await this._baseApis.sendToDevice("m.room.encrypted", contentMap);
|
||||
}
|
||||
};
|
||||
|
||||
registerAlgorithm(
|
||||
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption,
|
||||
);
|
||||
|
||||
@@ -20,11 +20,11 @@ limitations under the License.
|
||||
* @module crypto/algorithms/olm
|
||||
*/
|
||||
|
||||
import {logger} from '../../logger';
|
||||
import { logger } from '../../logger';
|
||||
import * as utils from "../../utils";
|
||||
import {polyfillSuper} from "../../utils";
|
||||
import { polyfillSuper } from "../../utils";
|
||||
import * as olmlib from "../olmlib";
|
||||
import {DeviceInfo} from "../deviceinfo";
|
||||
import { DeviceInfo } from "../deviceinfo";
|
||||
import {
|
||||
DecryptionAlgorithm,
|
||||
DecryptionError,
|
||||
@@ -95,7 +95,7 @@ OlmEncryption.prototype.encryptMessage = async function(room, eventType, content
|
||||
|
||||
const members = await room.getEncryptionTargetMembers();
|
||||
|
||||
const users = utils.map(members, function(u) {
|
||||
const users = members.map(function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
@@ -358,5 +358,4 @@ OlmDecryption.prototype._reallyDecryptMessage = async function(
|
||||
return res.payload;
|
||||
};
|
||||
|
||||
|
||||
registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
Copyright 2021 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 { DeviceInfo } from "./deviceinfo";
|
||||
import { IKeyBackupVersion } from "./keybackup";
|
||||
import { ISecretStorageKeyInfo } from "../matrix";
|
||||
|
||||
// TODO: Merge this with crypto.js once converted
|
||||
|
||||
export enum CrossSigningKey {
|
||||
Master = "master",
|
||||
SelfSigning = "self_signing",
|
||||
UserSigning = "user_signing",
|
||||
}
|
||||
|
||||
export interface IEncryptedEventInfo {
|
||||
/**
|
||||
* whether the event is encrypted (if not encrypted, some of the other properties may not be set)
|
||||
*/
|
||||
encrypted: boolean;
|
||||
|
||||
/**
|
||||
* the sender's key
|
||||
*/
|
||||
senderKey: string;
|
||||
|
||||
/**
|
||||
* the algorithm used to encrypt the event
|
||||
*/
|
||||
algorithm: string;
|
||||
|
||||
/**
|
||||
* whether we can be sure that the owner of the senderKey sent the event
|
||||
*/
|
||||
authenticated: boolean;
|
||||
|
||||
/**
|
||||
* the sender's device information, if available
|
||||
*/
|
||||
sender?: DeviceInfo;
|
||||
|
||||
/**
|
||||
* if the event's ed25519 and curve25519 keys don't match (only meaningful if `sender` is set)
|
||||
*/
|
||||
mismatchedSender: boolean;
|
||||
}
|
||||
|
||||
export interface IRecoveryKey {
|
||||
keyInfo: {
|
||||
pubkey: Uint8Array;
|
||||
passphrase?: {
|
||||
algorithm: string;
|
||||
iterations: number;
|
||||
salt: string;
|
||||
};
|
||||
};
|
||||
privateKey: Uint8Array;
|
||||
encodedPrivateKey: string;
|
||||
}
|
||||
|
||||
export interface ICreateSecretStorageOpts {
|
||||
/**
|
||||
* Function called to await a secret storage key creation flow.
|
||||
* Returns:
|
||||
* {Promise<Object>} Object with public key metadata, encoded private
|
||||
* recovery key which should be disposed of after displaying to the user,
|
||||
* and raw private key to avoid round tripping if needed.
|
||||
*/
|
||||
createSecretStorageKey?: () => Promise<IRecoveryKey>;
|
||||
|
||||
/**
|
||||
* The current key backup object. If passed,
|
||||
* the passphrase and recovery key from this backup will be used.
|
||||
*/
|
||||
keyBackupInfo?: IKeyBackupVersion;
|
||||
|
||||
/**
|
||||
* If true, a new key backup version will be
|
||||
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
|
||||
* is supplied.
|
||||
*/
|
||||
setupNewKeyBackup?: boolean;
|
||||
|
||||
/**
|
||||
* Reset even if keys already exist.
|
||||
*/
|
||||
setupNewSecretStorage?: boolean;
|
||||
|
||||
/**
|
||||
* Function called to get the user's
|
||||
* current key backup passphrase. Should return a promise that resolves with a Uint8Array
|
||||
* containing the key, or rejects if the key cannot be obtained.
|
||||
*/
|
||||
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
export interface ISecretStorageKey {
|
||||
keyId: string;
|
||||
keyInfo: ISecretStorageKeyInfo;
|
||||
}
|
||||
|
||||
export interface IAddSecretStorageKeyOpts {
|
||||
// depends on algorithm
|
||||
// TODO: Types
|
||||
}
|
||||
|
||||
export interface IImportOpts {
|
||||
stage: string; // TODO: Enum
|
||||
successes: number;
|
||||
failures: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface IImportRoomKeysOpts {
|
||||
progressCallback: (stage: IImportOpts) => void;
|
||||
untrusted?: boolean;
|
||||
source?: string; // TODO: Enum
|
||||
}
|
||||
@@ -0,0 +1,651 @@
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module crypto/backup
|
||||
*
|
||||
* Classes for dealing with key backup.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "../client";
|
||||
import { logger } from "../logger";
|
||||
import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { DeviceTrustLevel } from './CrossSigning';
|
||||
import { keyFromPassphrase } from './key_passphrase';
|
||||
import { sleep } from "../utils";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { encodeRecoveryKey } from './recoverykey';
|
||||
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
|
||||
type AuthData = Record<string, any>;
|
||||
|
||||
type BackupInfo = {
|
||||
algorithm: string,
|
||||
auth_data: AuthData, // eslint-disable-line camelcase
|
||||
[properties: string]: any,
|
||||
};
|
||||
|
||||
type SigInfo = {
|
||||
deviceId: string,
|
||||
valid?: boolean | null, // true: valid, false: invalid, null: cannot attempt validation
|
||||
device?: DeviceInfo | null,
|
||||
crossSigningId?: boolean,
|
||||
deviceTrust?: DeviceTrustLevel,
|
||||
};
|
||||
|
||||
type TrustInfo = {
|
||||
usable: boolean, // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
||||
sigs: SigInfo[],
|
||||
};
|
||||
|
||||
/** A function used to get the secret key for a backup.
|
||||
*/
|
||||
type GetKey = () => Promise<Uint8Array>;
|
||||
|
||||
interface BackupAlgorithmClass {
|
||||
algorithmName: string;
|
||||
// initialize from an existing backup
|
||||
init(authData: AuthData, getKey: GetKey): Promise<BackupAlgorithm>;
|
||||
|
||||
// prepare a brand new backup
|
||||
prepare(
|
||||
key: string | Uint8Array | null,
|
||||
): Promise<[Uint8Array, AuthData]>;
|
||||
}
|
||||
|
||||
interface BackupAlgorithm {
|
||||
encryptSession(data: Record<string, any>): Promise<any>;
|
||||
decryptSessions(ciphertexts: Record<string, any>): Promise<Record<string, any>[]>;
|
||||
authData: AuthData;
|
||||
keyMatches(key: Uint8Array): Promise<boolean>;
|
||||
free(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the key backup.
|
||||
*/
|
||||
export class BackupManager {
|
||||
private algorithm: BackupAlgorithm | undefined;
|
||||
private backupInfo: BackupInfo | undefined; // The info dict from /room_keys/version
|
||||
public checkedForBackup: boolean; // Have we checked the server for a backup we can use?
|
||||
private sendingBackups: boolean; // Are we currently sending backups?
|
||||
constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) {
|
||||
this.checkedForBackup = false;
|
||||
this.sendingBackups = false;
|
||||
}
|
||||
|
||||
public get version(): string | undefined {
|
||||
return this.backupInfo && this.backupInfo.version;
|
||||
}
|
||||
|
||||
public static async makeAlgorithm(info: BackupInfo, getKey: GetKey): Promise<BackupAlgorithm> {
|
||||
const Algorithm = algorithmsByName[info.algorithm];
|
||||
if (!Algorithm) {
|
||||
throw new Error("Unknown backup algorithm");
|
||||
}
|
||||
return await Algorithm.init(info.auth_data, getKey);
|
||||
}
|
||||
|
||||
public async enableKeyBackup(info: BackupInfo): Promise<void> {
|
||||
this.backupInfo = info;
|
||||
if (this.algorithm) {
|
||||
this.algorithm.free();
|
||||
}
|
||||
|
||||
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
|
||||
|
||||
this.baseApis.emit('crypto.keyBackupStatus', true);
|
||||
|
||||
// There may be keys left over from a partially completed backup, so
|
||||
// schedule a send to check.
|
||||
this.scheduleKeyBackupSend();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable backing up of keys.
|
||||
*/
|
||||
public disableKeyBackup(): void {
|
||||
if (this.algorithm) {
|
||||
this.algorithm.free();
|
||||
}
|
||||
this.algorithm = undefined;
|
||||
|
||||
this.backupInfo = undefined;
|
||||
|
||||
this.baseApis.emit('crypto.keyBackupStatus', false);
|
||||
}
|
||||
|
||||
public getKeyBackupEnabled(): boolean | null {
|
||||
if (!this.checkedForBackup) {
|
||||
return null;
|
||||
}
|
||||
return Boolean(this.algorithm);
|
||||
}
|
||||
|
||||
public async prepareKeyBackupVersion(
|
||||
key?: string | Uint8Array | null,
|
||||
algorithm?: string | undefined,
|
||||
): Promise<BackupInfo> {
|
||||
const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm;
|
||||
if (!Algorithm) {
|
||||
throw new Error("Unknown backup algorithm");
|
||||
}
|
||||
|
||||
const [privateKey, authData] = await Algorithm.prepare(key);
|
||||
const recoveryKey = encodeRecoveryKey(privateKey);
|
||||
return {
|
||||
algorithm: Algorithm.algorithmName,
|
||||
auth_data: authData,
|
||||
recovery_key: recoveryKey,
|
||||
privateKey,
|
||||
};
|
||||
}
|
||||
|
||||
public async createKeyBackupVersion(info: BackupInfo): Promise<void> {
|
||||
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the server for an active key backup and
|
||||
* if one is present and has a valid signature from
|
||||
* one of the user's verified devices, start backing up
|
||||
* to it.
|
||||
*/
|
||||
public async checkAndStart(): Promise<{backupInfo: BackupInfo, trustInfo: TrustInfo}> {
|
||||
logger.log("Checking key backup status...");
|
||||
if (this.baseApis.isGuest()) {
|
||||
logger.log("Skipping key backup check since user is guest");
|
||||
this.checkedForBackup = true;
|
||||
return null;
|
||||
}
|
||||
let backupInfo: BackupInfo;
|
||||
try {
|
||||
backupInfo = await this.baseApis.getKeyBackupVersion();
|
||||
} catch (e) {
|
||||
logger.log("Error checking for active key backup", e);
|
||||
if (e.httpStatus === 404) {
|
||||
// 404 is returned when the key backup does not exist, so that
|
||||
// counts as successfully checking.
|
||||
this.checkedForBackup = true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
this.checkedForBackup = true;
|
||||
|
||||
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
|
||||
|
||||
if (trustInfo.usable && !this.backupInfo) {
|
||||
logger.log(
|
||||
"Found usable key backup v" + backupInfo.version +
|
||||
": enabling key backups",
|
||||
);
|
||||
await this.enableKeyBackup(backupInfo);
|
||||
} else if (!trustInfo.usable && this.backupInfo) {
|
||||
logger.log("No usable key backup: disabling key backup");
|
||||
this.disableKeyBackup();
|
||||
} else if (!trustInfo.usable && !this.backupInfo) {
|
||||
logger.log("No usable key backup: not enabling key backup");
|
||||
} else if (trustInfo.usable && this.backupInfo) {
|
||||
// may not be the same version: if not, we should switch
|
||||
if (backupInfo.version !== this.backupInfo.version) {
|
||||
logger.log(
|
||||
"On backup version " + this.backupInfo.version + " but found " +
|
||||
"version " + backupInfo.version + ": switching.",
|
||||
);
|
||||
this.disableKeyBackup();
|
||||
await this.enableKeyBackup(backupInfo);
|
||||
// We're now using a new backup, so schedule all the keys we have to be
|
||||
// uploaded to the new backup. This is a bit of a workaround to upload
|
||||
// keys to a new backup in *most* cases, but it won't cover all cases
|
||||
// because we don't remember what backup version we uploaded keys to:
|
||||
// see https://github.com/vector-im/element-web/issues/14833
|
||||
await this.scheduleAllGroupSessionsForBackup();
|
||||
} else {
|
||||
logger.log("Backup version " + backupInfo.version + " still current");
|
||||
}
|
||||
}
|
||||
|
||||
return { backupInfo, trustInfo };
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces a re-check of the key backup and enables/disables it
|
||||
* as appropriate.
|
||||
*
|
||||
* @return {Object} Object with backup info (as returned by
|
||||
* getKeyBackupVersion) in backupInfo and
|
||||
* trust information (as returned by isKeyBackupTrusted)
|
||||
* in trustInfo.
|
||||
*/
|
||||
public async checkKeyBackup(): Promise<{backupInfo: BackupInfo, trustInfo: TrustInfo}> {
|
||||
this.checkedForBackup = false;
|
||||
return this.checkAndStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given backup info is trusted.
|
||||
*
|
||||
* @param {object} backupInfo key backup info dict from /room_keys/version
|
||||
* @return {object} {
|
||||
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
||||
* sigs: [
|
||||
* valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation
|
||||
* deviceId: [string],
|
||||
* device: [DeviceInfo || null],
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
public async isKeyBackupTrusted(backupInfo: BackupInfo): Promise<TrustInfo> {
|
||||
const ret = {
|
||||
usable: false,
|
||||
trusted_locally: false,
|
||||
sigs: [],
|
||||
};
|
||||
|
||||
if (
|
||||
!backupInfo ||
|
||||
!backupInfo.algorithm ||
|
||||
!backupInfo.auth_data ||
|
||||
!backupInfo.auth_data.public_key ||
|
||||
!backupInfo.auth_data.signatures
|
||||
) {
|
||||
logger.info("Key backup is absent or missing required data");
|
||||
return ret;
|
||||
}
|
||||
|
||||
const trustedPubkey = this.baseApis.crypto._sessionStore.getLocalTrustedBackupPubKey();
|
||||
|
||||
if (backupInfo.auth_data.public_key === trustedPubkey) {
|
||||
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
|
||||
ret.trusted_locally = true;
|
||||
}
|
||||
|
||||
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || [];
|
||||
|
||||
for (const keyId of Object.keys(mySigs)) {
|
||||
const keyIdParts = keyId.split(':');
|
||||
if (keyIdParts[0] !== 'ed25519') {
|
||||
logger.log("Ignoring unknown signature type: " + keyIdParts[0]);
|
||||
continue;
|
||||
}
|
||||
// Could be a cross-signing master key, but just say this is the device
|
||||
// ID for backwards compat
|
||||
const sigInfo: SigInfo = { deviceId: keyIdParts[1] };
|
||||
|
||||
// first check to see if it's from our cross-signing key
|
||||
const crossSigningId = this.baseApis.crypto._crossSigningInfo.getId();
|
||||
if (crossSigningId === sigInfo.deviceId) {
|
||||
sigInfo.crossSigningId = true;
|
||||
try {
|
||||
await verifySignature(
|
||||
this.baseApis.crypto._olmDevice,
|
||||
backupInfo.auth_data,
|
||||
this.baseApis.getUserId(),
|
||||
sigInfo.deviceId,
|
||||
crossSigningId,
|
||||
);
|
||||
sigInfo.valid = true;
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
"Bad signature from cross signing key " + crossSigningId, e,
|
||||
);
|
||||
sigInfo.valid = false;
|
||||
}
|
||||
ret.sigs.push(sigInfo);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now look for a sig from a device
|
||||
// At some point this can probably go away and we'll just support
|
||||
// it being signed by the cross-signing master key
|
||||
const device = this.baseApis.crypto._deviceList.getStoredDevice(
|
||||
this.baseApis.getUserId(), sigInfo.deviceId,
|
||||
);
|
||||
if (device) {
|
||||
sigInfo.device = device;
|
||||
sigInfo.deviceTrust = await this.baseApis.checkDeviceTrust(
|
||||
this.baseApis.getUserId(), sigInfo.deviceId,
|
||||
);
|
||||
try {
|
||||
await verifySignature(
|
||||
this.baseApis.crypto._olmDevice,
|
||||
backupInfo.auth_data,
|
||||
this.baseApis.getUserId(),
|
||||
device.deviceId,
|
||||
device.getFingerprint(),
|
||||
);
|
||||
sigInfo.valid = true;
|
||||
} catch (e) {
|
||||
logger.info(
|
||||
"Bad signature from key ID " + keyId + " userID " + this.baseApis.getUserId() +
|
||||
" device ID " + device.deviceId + " fingerprint: " +
|
||||
device.getFingerprint(), backupInfo.auth_data, e,
|
||||
);
|
||||
sigInfo.valid = false;
|
||||
}
|
||||
} else {
|
||||
sigInfo.valid = null; // Can't determine validity because we don't have the signing device
|
||||
logger.info("Ignoring signature from unknown key " + keyId);
|
||||
}
|
||||
ret.sigs.push(sigInfo);
|
||||
}
|
||||
|
||||
ret.usable = ret.sigs.some((s) => {
|
||||
return (
|
||||
s.valid && (
|
||||
(s.device && s.deviceTrust.isVerified()) ||
|
||||
(s.crossSigningId)
|
||||
)
|
||||
);
|
||||
});
|
||||
ret.usable = ret.usable || ret.trusted_locally;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules sending all keys waiting to be sent to the backup, if not already
|
||||
* scheduled. Retries if necessary.
|
||||
*
|
||||
* @param maxDelay Maximum delay to wait in ms. 0 means no delay.
|
||||
*/
|
||||
public async scheduleKeyBackupSend(maxDelay = 10000): Promise<void> {
|
||||
if (this.sendingBackups) return;
|
||||
|
||||
this.sendingBackups = true;
|
||||
|
||||
try {
|
||||
// wait between 0 and `maxDelay` seconds, to avoid backup
|
||||
// requests from different clients hitting the server all at
|
||||
// the same time when a new key is sent
|
||||
const delay = Math.random() * maxDelay;
|
||||
await sleep(delay, undefined);
|
||||
let numFailures = 0; // number of consecutive failures
|
||||
for (;;) {
|
||||
if (!this.algorithm) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const numBackedUp =
|
||||
await this.backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
|
||||
if (numBackedUp === 0) {
|
||||
// no sessions left needing backup: we're done
|
||||
return;
|
||||
}
|
||||
numFailures = 0;
|
||||
} catch (err) {
|
||||
numFailures++;
|
||||
logger.log("Key backup request failed", err);
|
||||
if (err.data) {
|
||||
if (
|
||||
err.data.errcode == 'M_NOT_FOUND' ||
|
||||
err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
|
||||
) {
|
||||
// Re-check key backup status on error, so we can be
|
||||
// sure to present the current situation when asked.
|
||||
await this.checkKeyBackup();
|
||||
// Backup version has changed or this backup version
|
||||
// has been deleted
|
||||
this.baseApis.crypto.emit("crypto.keyBackupFailed", err.data.errcode);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (numFailures) {
|
||||
// exponential backoff if we have failures
|
||||
await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)), undefined);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.sendingBackups = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take some e2e keys waiting to be backed up and send them
|
||||
* to the backup.
|
||||
*
|
||||
* @param {integer} limit Maximum number of keys to back up
|
||||
* @returns {integer} Number of sessions backed up
|
||||
*/
|
||||
private async backupPendingKeys(limit: number): Promise<number> {
|
||||
const sessions = await this.baseApis.crypto._cryptoStore.getSessionsNeedingBackup(limit);
|
||||
if (!sessions.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let remaining = await this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
const data = {};
|
||||
for (const session of sessions) {
|
||||
const roomId = session.sessionData.room_id;
|
||||
if (data[roomId] === undefined) {
|
||||
data[roomId] = { sessions: {} };
|
||||
}
|
||||
|
||||
const sessionData = await this.baseApis.crypto._olmDevice.exportInboundGroupSession(
|
||||
session.senderKey, session.sessionId, session.sessionData,
|
||||
);
|
||||
sessionData.algorithm = MEGOLM_ALGORITHM;
|
||||
|
||||
const forwardedCount =
|
||||
(sessionData.forwarding_curve25519_key_chain || []).length;
|
||||
|
||||
const userId = this.baseApis.crypto._deviceList.getUserByIdentityKey(
|
||||
MEGOLM_ALGORITHM, session.senderKey,
|
||||
);
|
||||
const device = this.baseApis.crypto._deviceList.getDeviceByIdentityKey(
|
||||
MEGOLM_ALGORITHM, session.senderKey,
|
||||
);
|
||||
const verified = this.baseApis.crypto._checkDeviceInfoTrust(userId, device).isVerified();
|
||||
|
||||
data[roomId]['sessions'][session.sessionId] = {
|
||||
first_message_index: sessionData.first_known_index,
|
||||
forwarded_count: forwardedCount,
|
||||
is_verified: verified,
|
||||
session_data: await this.algorithm.encryptSession(sessionData),
|
||||
};
|
||||
}
|
||||
|
||||
await this.baseApis.sendKeyBackup(
|
||||
undefined, undefined, this.backupInfo.version,
|
||||
{ rooms: data },
|
||||
);
|
||||
|
||||
await this.baseApis.crypto._cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
||||
remaining = await this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
return sessions.length;
|
||||
}
|
||||
|
||||
public async backupGroupSession(
|
||||
senderKey: string, sessionId: string,
|
||||
): Promise<void> {
|
||||
await this.baseApis.crypto._cryptoStore.markSessionsNeedingBackup([{
|
||||
senderKey: senderKey,
|
||||
sessionId: sessionId,
|
||||
}]);
|
||||
|
||||
if (this.backupInfo) {
|
||||
// don't wait for this to complete: it will delay so
|
||||
// happens in the background
|
||||
this.scheduleKeyBackupSend();
|
||||
}
|
||||
// if this.backupInfo is not set, then the keys will be backed up when
|
||||
// this.enableKeyBackup is called
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks all group sessions as needing to be backed up and schedules them to
|
||||
* upload in the background as soon as possible.
|
||||
*/
|
||||
public async scheduleAllGroupSessionsForBackup(): Promise<void> {
|
||||
await this.flagAllGroupSessionsForBackup();
|
||||
|
||||
// Schedule keys to upload in the background as soon as possible.
|
||||
this.scheduleKeyBackupSend(0 /* maxDelay */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks all group sessions as needing to be backed up without scheduling
|
||||
* them to upload in the background.
|
||||
* @returns {Promise<int>} Resolves to the number of sessions now requiring a backup
|
||||
* (which will be equal to the number of sessions in the store).
|
||||
*/
|
||||
public async flagAllGroupSessionsForBackup(): Promise<number> {
|
||||
await this.baseApis.crypto._cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_BACKUP,
|
||||
],
|
||||
(txn) => {
|
||||
this.baseApis.crypto._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
|
||||
if (session !== null) {
|
||||
this.baseApis.crypto._cryptoStore.markSessionsNeedingBackup([session], txn);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const remaining = await this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
return remaining;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of end to end session keys that are waiting to be backed up
|
||||
* @returns {Promise<int>} Resolves to the number of sessions requiring backup
|
||||
*/
|
||||
public countSessionsNeedingBackup(): Promise<number> {
|
||||
return this.baseApis.crypto._cryptoStore.countSessionsNeedingBackup();
|
||||
}
|
||||
}
|
||||
|
||||
export class Curve25519 implements BackupAlgorithm {
|
||||
public static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2";
|
||||
|
||||
constructor(
|
||||
public authData: AuthData,
|
||||
private publicKey: any, // FIXME: PkEncryption
|
||||
private getKey: () => Promise<Uint8Array>,
|
||||
) {}
|
||||
|
||||
public static async init(
|
||||
authData: AuthData,
|
||||
getKey: () => Promise<Uint8Array>,
|
||||
): Promise<Curve25519> {
|
||||
if (!authData || !authData.public_key) {
|
||||
throw new Error("auth_data missing required information");
|
||||
}
|
||||
const publicKey = new global.Olm.PkEncryption();
|
||||
publicKey.set_recipient_key(authData.public_key);
|
||||
return new Curve25519(authData, publicKey, getKey);
|
||||
}
|
||||
|
||||
public static async prepare(
|
||||
key: string | Uint8Array | null,
|
||||
): Promise<[Uint8Array, AuthData]> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
const authData: AuthData = {};
|
||||
if (!key) {
|
||||
authData.public_key = decryption.generate_key();
|
||||
} else if (key instanceof Uint8Array) {
|
||||
authData.public_key = decryption.init_with_private_key(key);
|
||||
} else {
|
||||
const derivation = await keyFromPassphrase(key);
|
||||
authData.private_key_salt = derivation.salt;
|
||||
authData.private_key_iterations = derivation.iterations;
|
||||
authData.public_key = decryption.init_with_private_key(derivation.key);
|
||||
}
|
||||
const publicKey = new global.Olm.PkEncryption();
|
||||
publicKey.set_recipient_key(authData.public_key);
|
||||
|
||||
return [
|
||||
decryption.get_private_key(),
|
||||
authData,
|
||||
];
|
||||
} finally {
|
||||
decryption.free();
|
||||
}
|
||||
}
|
||||
|
||||
public async encryptSession(data: Record<string, any>): Promise<any> {
|
||||
const plainText: Record<string, any> = Object.assign({}, data);
|
||||
delete plainText.session_id;
|
||||
delete plainText.room_id;
|
||||
delete plainText.first_known_index;
|
||||
return this.publicKey.encrypt(JSON.stringify(plainText));
|
||||
}
|
||||
|
||||
public async decryptSessions(sessions: Record<string, Record<string, any>>): Promise<Record<string, any>[]> {
|
||||
const privKey = await this.getKey();
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
const backupPubKey = decryption.init_with_private_key(privKey);
|
||||
|
||||
if (backupPubKey !== this.authData.public_key) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw { errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY };
|
||||
}
|
||||
|
||||
const keys = [];
|
||||
|
||||
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
||||
try {
|
||||
const decrypted = JSON.parse(decryption.decrypt(
|
||||
sessionData.session_data.ephemeral,
|
||||
sessionData.session_data.mac,
|
||||
sessionData.session_data.ciphertext,
|
||||
));
|
||||
decrypted.session_id = sessionId;
|
||||
keys.push(decrypted);
|
||||
} catch (e) {
|
||||
logger.log("Failed to decrypt megolm session from backup", e, sessionData);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
} finally {
|
||||
decryption.free();
|
||||
}
|
||||
}
|
||||
|
||||
public async keyMatches(key: Uint8Array): Promise<boolean> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
let pubKey;
|
||||
try {
|
||||
pubKey = decryption.init_with_private_key(key);
|
||||
} finally {
|
||||
decryption.free();
|
||||
}
|
||||
|
||||
return pubKey === this.authData.public_key;
|
||||
}
|
||||
|
||||
public free(): void {
|
||||
this.publicKey.free();
|
||||
}
|
||||
}
|
||||
|
||||
export const algorithmsByName: Record<string, BackupAlgorithmClass> = {
|
||||
[Curve25519.algorithmName]: Curve25519,
|
||||
};
|
||||
|
||||
export const DefaultAlgorithm: BackupAlgorithmClass = Curve25519;
|
||||
+54
-18
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020-2021 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.
|
||||
@@ -14,15 +14,28 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {decodeBase64, encodeBase64} from './olmlib';
|
||||
import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store';
|
||||
import {decryptAES, encryptAES} from './aes';
|
||||
import { decodeBase64, encodeBase64 } from './olmlib';
|
||||
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
|
||||
import { decryptAES, encryptAES } from './aes';
|
||||
import anotherjson from "another-json";
|
||||
import {logger} from '../logger';
|
||||
import { logger } from '../logger';
|
||||
import { ISecretStorageKeyInfo } from "../matrix";
|
||||
|
||||
// FIXME: these types should eventually go in a different file
|
||||
type Signatures = Record<string, Record<string, string>>;
|
||||
|
||||
export interface IDehydratedDevice {
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
device_data: ISecretStorageKeyInfo & { // eslint-disable-line camelcase
|
||||
algorithm: string;
|
||||
account: string; // pickle
|
||||
};
|
||||
}
|
||||
|
||||
export interface IDehydratedDeviceKeyInfo {
|
||||
passphrase?: string;
|
||||
}
|
||||
|
||||
interface DeviceKeys {
|
||||
algorithms: Array<string>;
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
@@ -51,7 +64,7 @@ export class DehydrationManager {
|
||||
this.getDehydrationKeyFromCache();
|
||||
}
|
||||
async getDehydrationKeyFromCache(): Promise<void> {
|
||||
return this.crypto._cryptoStore.doTxn(
|
||||
return await this.crypto._cryptoStore.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
@@ -59,7 +72,7 @@ export class DehydrationManager {
|
||||
txn,
|
||||
async (result) => {
|
||||
if (result) {
|
||||
const {key, keyInfo, deviceDisplayName, time} = result;
|
||||
const { key, keyInfo, deviceDisplayName, time } = result;
|
||||
const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey);
|
||||
const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
|
||||
this.key = decodeBase64(decrypted);
|
||||
@@ -77,10 +90,23 @@ export class DehydrationManager {
|
||||
},
|
||||
);
|
||||
}
|
||||
async setDehydrationKey(
|
||||
|
||||
/** set the key, and queue periodic dehydration to the server in the background */
|
||||
async setKeyAndQueueDehydration(
|
||||
key: Uint8Array, keyInfo: {[props: string]: any} = {},
|
||||
deviceDisplayName: string = undefined,
|
||||
): Promise<void> {
|
||||
const matches = await this.setKey(key, keyInfo, deviceDisplayName);
|
||||
if (!matches) {
|
||||
// start dehydration in the background
|
||||
this.dehydrateDevice();
|
||||
}
|
||||
}
|
||||
|
||||
async setKey(
|
||||
key: Uint8Array, keyInfo: {[props: string]: any} = {},
|
||||
deviceDisplayName: string = undefined,
|
||||
): Promise<boolean> {
|
||||
if (!key) {
|
||||
// unsetting the key -- cancel any pending dehydration task
|
||||
if (this.timeoutId) {
|
||||
@@ -88,7 +114,7 @@ export class DehydrationManager {
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
// clear storage
|
||||
this.crypto._cryptoStore.doTxn(
|
||||
await this.crypto._cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
@@ -104,7 +130,7 @@ export class DehydrationManager {
|
||||
|
||||
// Check to see if it's the same key as before. If it's different,
|
||||
// dehydrate a new device. If it's the same, we can keep the same
|
||||
// device. (Assume that keyInfo and deviceDisplayNamme will be the
|
||||
// device. (Assume that keyInfo and deviceDisplayName will be the
|
||||
// same if the key is the same.)
|
||||
let matches: boolean = this.key && key.length == this.key.length;
|
||||
for (let i = 0; matches && i < key.length; i++) {
|
||||
@@ -116,11 +142,12 @@ export class DehydrationManager {
|
||||
this.key = key;
|
||||
this.keyInfo = keyInfo;
|
||||
this.deviceDisplayName = deviceDisplayName;
|
||||
// start dehydration in the background
|
||||
this.dehydrateDevice();
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
private async dehydrateDevice(): Promise<void> {
|
||||
|
||||
/** returns the device id of the newly created dehydrated device */
|
||||
async dehydrateDevice(): Promise<string> {
|
||||
if (this.inProgress) {
|
||||
logger.log("Dehydration already in progress -- not starting new dehydration");
|
||||
return;
|
||||
@@ -135,7 +162,7 @@ export class DehydrationManager {
|
||||
|
||||
// update the crypto store with the timestamp
|
||||
const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM);
|
||||
this.crypto._cryptoStore.doTxn(
|
||||
await this.crypto._cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
@@ -178,7 +205,7 @@ export class DehydrationManager {
|
||||
}
|
||||
|
||||
logger.log("Uploading account to server");
|
||||
const dehydrateResult = await this.crypto._baseApis._http.authedRequest(
|
||||
const dehydrateResult = await this.crypto._baseApis.http.authedRequest(
|
||||
undefined,
|
||||
"PUT",
|
||||
"/dehydrated_device",
|
||||
@@ -217,7 +244,7 @@ export class DehydrationManager {
|
||||
logger.log("Preparing one-time keys");
|
||||
const oneTimeKeys = {};
|
||||
for (const [keyId, key] of Object.entries(otks.curve25519)) {
|
||||
const k: OneTimeKey = {key};
|
||||
const k: OneTimeKey = { key };
|
||||
const signature = account.sign(anotherjson.stringify(k));
|
||||
k.signatures = {
|
||||
[this.crypto._userId]: {
|
||||
@@ -230,7 +257,7 @@ export class DehydrationManager {
|
||||
logger.log("Preparing fallback keys");
|
||||
const fallbackKeys = {};
|
||||
for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
|
||||
const k: OneTimeKey = {key, fallback: true};
|
||||
const k: OneTimeKey = { key, fallback: true };
|
||||
const signature = account.sign(anotherjson.stringify(k));
|
||||
k.signatures = {
|
||||
[this.crypto._userId]: {
|
||||
@@ -241,7 +268,7 @@ export class DehydrationManager {
|
||||
}
|
||||
|
||||
logger.log("Uploading keys to server");
|
||||
await this.crypto._baseApis._http.authedRequest(
|
||||
await this.crypto._baseApis.http.authedRequest(
|
||||
undefined,
|
||||
"POST",
|
||||
"/keys/upload/" + encodeURI(deviceId),
|
||||
@@ -258,8 +285,17 @@ export class DehydrationManager {
|
||||
this.timeoutId = global.setTimeout(
|
||||
this.dehydrateDevice.bind(this), oneweek,
|
||||
);
|
||||
|
||||
return deviceId;
|
||||
} finally {
|
||||
this.inProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private stop() {
|
||||
if (this.timeoutId) {
|
||||
global.clearTimeout(this.timeoutId);
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+173
-489
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {randomString} from '../randomstring';
|
||||
import { randomString } from '../randomstring';
|
||||
|
||||
const DEFAULT_ITERATIONS = 500000;
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function deriveKey(password, salt, iterations, numBits = DEFAULT_BI
|
||||
const key = await subtleCrypto.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
{name: 'PBKDF2'},
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveBits'],
|
||||
);
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
Copyright 2021 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 { ISignatures } from "../@types/signed";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
|
||||
export interface IKeyBackupSession {
|
||||
first_message_index: number; // eslint-disable-line camelcase
|
||||
forwarded_count: number; // eslint-disable-line camelcase
|
||||
is_verified: boolean; // eslint-disable-line camelcase
|
||||
session_data: { // eslint-disable-line camelcase
|
||||
ciphertext: string;
|
||||
ephemeral: string;
|
||||
mac: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IKeyBackupRoomSessions {
|
||||
[sessionId: string]: IKeyBackupSession;
|
||||
}
|
||||
|
||||
export interface IKeyBackupVersion {
|
||||
algorithm: string;
|
||||
auth_data: { // eslint-disable-line camelcase
|
||||
public_key: string; // eslint-disable-line camelcase
|
||||
signatures: ISignatures;
|
||||
};
|
||||
count: number;
|
||||
etag: string;
|
||||
version: string; // number contained within
|
||||
}
|
||||
|
||||
// TODO: Verify types
|
||||
export interface IKeyBackupTrustInfo {
|
||||
/**
|
||||
* is the backup trusted, true if there is a sig that is valid & from a trusted device
|
||||
*/
|
||||
usable: boolean[];
|
||||
sigs: {
|
||||
valid: boolean[];
|
||||
device: DeviceInfo[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface IKeyBackupPrepareOpts {
|
||||
secureSecretStorage: boolean;
|
||||
}
|
||||
|
||||
export interface IKeyBackupRestoreResult {
|
||||
total: number;
|
||||
imported: number;
|
||||
}
|
||||
|
||||
export interface IKeyBackupRestoreOpts {
|
||||
cacheCompleteCallback?: () => void;
|
||||
progressCallback?: ({ stage: string }) => void;
|
||||
}
|
||||
+64
-44
@@ -22,7 +22,7 @@ limitations under the License.
|
||||
* Utilities common to olm encryption algorithms
|
||||
*/
|
||||
|
||||
import {logger} from '../logger';
|
||||
import { logger } from '../logger';
|
||||
import * as utils from "../utils";
|
||||
import anotherjson from "another-json";
|
||||
|
||||
@@ -41,7 +41,6 @@ export const MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
|
||||
*/
|
||||
export const MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2";
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt an event payload for an Olm device
|
||||
*
|
||||
@@ -119,7 +118,7 @@ export async function encryptMessageForDevice(
|
||||
*
|
||||
* @param {module:crypto/OlmDevice} olmDevice
|
||||
*
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis
|
||||
* @param {MatrixClient} baseApis
|
||||
*
|
||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||
* map from userid to list of devices to ensure sessions for
|
||||
@@ -169,7 +168,7 @@ export async function getExistingOlmSessions(
|
||||
*
|
||||
* @param {module:crypto/OlmDevice} olmDevice
|
||||
*
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis
|
||||
* @param {MatrixClient} baseApis
|
||||
*
|
||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||
* map from userid to list of devices to ensure sessions for
|
||||
@@ -183,18 +182,24 @@ export async function getExistingOlmSessions(
|
||||
* @param {Array} [failedServers] An array to fill with remote servers that
|
||||
* failed to respond to one-time-key requests.
|
||||
*
|
||||
* @param {Logger} [log] A possibly customised log
|
||||
*
|
||||
* @return {Promise} resolves once the sessions are complete, to
|
||||
* an Object mapping from userId to deviceId to
|
||||
* {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
export async function ensureOlmSessionsForDevices(
|
||||
olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers,
|
||||
olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers, log,
|
||||
) {
|
||||
if (typeof force === "number") {
|
||||
log = failedServers;
|
||||
failedServers = otkTimeout;
|
||||
otkTimeout = force;
|
||||
force = false;
|
||||
}
|
||||
if (!log) {
|
||||
log = logger;
|
||||
}
|
||||
|
||||
const devicesWithoutSession = [
|
||||
// [userId, deviceId], ...
|
||||
@@ -202,6 +207,35 @@ export async function ensureOlmSessionsForDevices(
|
||||
const result = {};
|
||||
const resolveSession = {};
|
||||
|
||||
// Mark all sessions this task intends to update as in progress. It is
|
||||
// important to do this for all devices this task cares about in a single
|
||||
// synchronous operation, as otherwise it is possible to have deadlocks
|
||||
// where multiple tasks wait indefinitely on another task to update some set
|
||||
// of common devices.
|
||||
for (const [, devices] of Object.entries(devicesByUser)) {
|
||||
for (const deviceInfo of devices) {
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
|
||||
if (key === olmDevice.deviceCurve25519Key) {
|
||||
// We don't start sessions with ourself, so there's no need to
|
||||
// mark it in progress.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!olmDevice._sessionsInProgress[key]) {
|
||||
// pre-emptively mark the session as in-progress to avoid race
|
||||
// conditions. If we find that we already have a session, then
|
||||
// we'll resolve
|
||||
olmDevice._sessionsInProgress[key] = new Promise(resolve => {
|
||||
resolveSession[key] = (...args) => {
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
resolve(...args);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
result[userId] = {};
|
||||
for (const deviceInfo of devices) {
|
||||
@@ -216,7 +250,7 @@ export async function ensureOlmSessionsForDevices(
|
||||
// new chain when this side has an active sender chain.
|
||||
// If you see this message being logged in the wild, we should find
|
||||
// the thing that is trying to send Olm messages to itself and fix it.
|
||||
logger.info("Attempted to start session with ourself! Ignoring");
|
||||
log.info("Attempted to start session with ourself! Ignoring");
|
||||
// We must fill in the section in the return value though, as callers
|
||||
// expect it to be there.
|
||||
result[userId][deviceId] = {
|
||||
@@ -226,41 +260,21 @@ export async function ensureOlmSessionsForDevices(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!olmDevice._sessionsInProgress[key]) {
|
||||
// pre-emptively mark the session as in-progress to avoid race
|
||||
// conditions. If we find that we already have a session, then
|
||||
// we'll resolve
|
||||
olmDevice._sessionsInProgress[key] = new Promise(
|
||||
(resolve, reject) => {
|
||||
resolveSession[key] = {
|
||||
resolve: (...args) => {
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
resolve(...args);
|
||||
},
|
||||
reject: (...args) => {
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
reject(...args);
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
const forWhom = `for ${key} (${userId}:${deviceId})`;
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(
|
||||
key, resolveSession[key],
|
||||
key, resolveSession[key], log,
|
||||
);
|
||||
if (sessionId !== null && resolveSession[key]) {
|
||||
// we found a session, but we had marked the session as
|
||||
// in-progress, so unmark it and unblock anything that was
|
||||
// waiting
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
resolveSession[key].resolve();
|
||||
delete resolveSession[key];
|
||||
// in-progress, so resolve it now, which will unmark it and
|
||||
// unblock anything that was waiting
|
||||
resolveSession[key]();
|
||||
}
|
||||
if (sessionId === null || force) {
|
||||
if (force) {
|
||||
logger.info("Forcing new Olm session for " + userId + ":" + deviceId);
|
||||
log.info(`Forcing new Olm session ${forWhom}`);
|
||||
} else {
|
||||
logger.info("Making new Olm session for " + userId + ":" + deviceId);
|
||||
log.info(`Making new Olm session ${forWhom}`);
|
||||
}
|
||||
devicesWithoutSession.push([userId, deviceId]);
|
||||
}
|
||||
@@ -277,15 +291,18 @@ export async function ensureOlmSessionsForDevices(
|
||||
|
||||
const oneTimeKeyAlgorithm = "signed_curve25519";
|
||||
let res;
|
||||
let taskDetail = `one-time keys for ${devicesWithoutSession.length} devices`;
|
||||
try {
|
||||
log.debug(`Claiming ${taskDetail}`);
|
||||
res = await baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout,
|
||||
);
|
||||
log.debug(`Claimed ${taskDetail}`);
|
||||
} catch (e) {
|
||||
for (const resolver of Object.values(resolveSession)) {
|
||||
resolver.resolve();
|
||||
resolver();
|
||||
}
|
||||
logger.log("failed to claim one-time keys", e, devicesWithoutSession);
|
||||
log.log(`Failed to claim ${taskDetail}`, e, devicesWithoutSession);
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -293,10 +310,10 @@ export async function ensureOlmSessionsForDevices(
|
||||
failedServers.push(...Object.keys(res.failures));
|
||||
}
|
||||
|
||||
const otk_res = res.one_time_keys || {};
|
||||
const otkResult = res.one_time_keys || {};
|
||||
const promises = [];
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
const userRes = otk_res[userId] || {};
|
||||
const userRes = otkResult[userId] || {};
|
||||
for (let j = 0; j < devices.length; j++) {
|
||||
const deviceInfo = devices[j];
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
@@ -323,11 +340,12 @@ export async function ensureOlmSessionsForDevices(
|
||||
}
|
||||
|
||||
if (!oneTimeKey) {
|
||||
const msg = "No one-time keys (alg=" + oneTimeKeyAlgorithm +
|
||||
") for device " + userId + ":" + deviceId;
|
||||
logger.warn(msg);
|
||||
log.warn(
|
||||
`No one-time keys (alg=${oneTimeKeyAlgorithm}) ` +
|
||||
`for device ${userId}:${deviceId}`,
|
||||
);
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key].resolve();
|
||||
resolveSession[key]();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -337,12 +355,12 @@ export async function ensureOlmSessionsForDevices(
|
||||
olmDevice, oneTimeKey, userId, deviceInfo,
|
||||
).then((sid) => {
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key].resolve(sid);
|
||||
resolveSession[key](sid);
|
||||
}
|
||||
result[userId][deviceId].sessionId = sid;
|
||||
}, (e) => {
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key].resolve();
|
||||
resolveSession[key]();
|
||||
}
|
||||
throw e;
|
||||
}),
|
||||
@@ -350,7 +368,10 @@ export async function ensureOlmSessionsForDevices(
|
||||
}
|
||||
}
|
||||
|
||||
taskDetail = `Olm sessions for ${promises.length} devices`;
|
||||
log.debug(`Starting ${taskDetail}`);
|
||||
await Promise.all(promises);
|
||||
log.debug(`Started ${taskDetail}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -386,7 +407,6 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
|
||||
return sid;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verify the signature on an object
|
||||
*
|
||||
|
||||
@@ -16,10 +16,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from '../../logger';
|
||||
import { logger } from '../../logger';
|
||||
import * as utils from "../../utils";
|
||||
|
||||
export const VERSION = 9;
|
||||
export const VERSION = 10;
|
||||
const PROFILE_TRANSACTIONS = false;
|
||||
|
||||
/**
|
||||
* Implementation of a CryptoStore which is backed by an existing
|
||||
@@ -34,6 +35,7 @@ export class Backend {
|
||||
*/
|
||||
constructor(db) {
|
||||
this._db = db;
|
||||
this._nextTxnId = 0;
|
||||
|
||||
// make sure we close the db on `onversionchange` - otherwise
|
||||
// attempts to delete the database will block (and subsequent
|
||||
@@ -228,7 +230,7 @@ export class Backend {
|
||||
const cursor = ev.target.result;
|
||||
if (cursor) {
|
||||
const keyReq = cursor.value;
|
||||
if (keyReq.recipients.includes({userId, deviceId})) {
|
||||
if (keyReq.recipients.includes({ userId, deviceId })) {
|
||||
results.push(keyReq);
|
||||
}
|
||||
cursor.continue();
|
||||
@@ -494,7 +496,7 @@ export class Backend {
|
||||
const lastProblem = problems[problems.length - 1];
|
||||
for (const problem of problems) {
|
||||
if (problem.time > timestamp) {
|
||||
result = Object.assign({}, problem, {fixed: lastProblem.fixed});
|
||||
result = Object.assign({}, problem, { fixed: lastProblem.fixed });
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -517,11 +519,11 @@ export class Backend {
|
||||
|
||||
await Promise.all(devices.map((device) => {
|
||||
return new Promise((resolve) => {
|
||||
const {userId, deviceInfo} = device;
|
||||
const { userId, deviceInfo } = device;
|
||||
const getReq = objectStore.get([userId, deviceInfo.deviceId]);
|
||||
getReq.onsuccess = function() {
|
||||
if (!getReq.result) {
|
||||
objectStore.put({userId, deviceId: deviceInfo.deviceId});
|
||||
objectStore.put({ userId, deviceId: deviceInfo.deviceId });
|
||||
ret.push(device);
|
||||
}
|
||||
resolve();
|
||||
@@ -757,10 +759,59 @@ export class Backend {
|
||||
}));
|
||||
}
|
||||
|
||||
doTxn(mode, stores, func) {
|
||||
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
|
||||
if (!txn) {
|
||||
txn = this._db.transaction(
|
||||
"shared_history_inbound_group_sessions", "readwrite",
|
||||
);
|
||||
}
|
||||
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
|
||||
const req = objectStore.get([roomId]);
|
||||
req.onsuccess = () => {
|
||||
const { sessions } = req.result || { sessions: [] };
|
||||
sessions.push([senderKey, sessionId]);
|
||||
objectStore.put({ roomId, sessions });
|
||||
};
|
||||
}
|
||||
|
||||
getSharedHistoryInboundGroupSessions(roomId, txn) {
|
||||
if (!txn) {
|
||||
txn = this._db.transaction(
|
||||
"shared_history_inbound_group_sessions", "readonly",
|
||||
);
|
||||
}
|
||||
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
|
||||
const req = objectStore.get([roomId]);
|
||||
return new Promise((resolve, reject) => {
|
||||
req.onsuccess = () => {
|
||||
const { sessions } = req.result || { sessions: [] };
|
||||
resolve(sessions);
|
||||
};
|
||||
req.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
doTxn(mode, stores, func, log = logger) {
|
||||
let startTime;
|
||||
let description;
|
||||
if (PROFILE_TRANSACTIONS) {
|
||||
const txnId = this._nextTxnId++;
|
||||
startTime = Date.now();
|
||||
description = `${mode} crypto store transaction ${txnId} in ${stores}`;
|
||||
log.debug(`Starting ${description}`);
|
||||
}
|
||||
const txn = this._db.transaction(stores, mode);
|
||||
const promise = promiseifyTxn(txn);
|
||||
const result = func(txn);
|
||||
if (PROFILE_TRANSACTIONS) {
|
||||
promise.then(() => {
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
log.debug(`Finished ${description}, took ${elapsedTime} ms`);
|
||||
}, () => {
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
log.error(`Failed ${description}, took ${elapsedTime} ms`);
|
||||
});
|
||||
}
|
||||
return promise.then(() => {
|
||||
return result;
|
||||
});
|
||||
@@ -815,6 +866,11 @@ export function upgradeDatabase(db, oldVersion) {
|
||||
keyPath: ["userId", "deviceId"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 10) {
|
||||
db.createObjectStore("shared_history_inbound_group_sessions", {
|
||||
keyPath: ["roomId"],
|
||||
});
|
||||
}
|
||||
// Expand as needed.
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from '../../logger';
|
||||
import {LocalStorageCryptoStore} from './localStorage-crypto-store';
|
||||
import {MemoryCryptoStore} from './memory-crypto-store';
|
||||
import { logger } from '../../logger';
|
||||
import { LocalStorageCryptoStore } from './localStorage-crypto-store';
|
||||
import { MemoryCryptoStore } from './memory-crypto-store';
|
||||
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
|
||||
import {InvalidCryptoStoreError} from '../../errors';
|
||||
import { InvalidCryptoStoreError } from '../../errors';
|
||||
import * as IndexedDBHelpers from "../../indexeddb-helpers";
|
||||
|
||||
/**
|
||||
@@ -582,6 +582,29 @@ export class IndexedDBCryptoStore {
|
||||
return this._backend.markSessionsNeedingBackup(sessions, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared-history group session for a room.
|
||||
* @param {string} roomId The room that the key belongs to
|
||||
* @param {string} senderKey The sender's curve 25519 key
|
||||
* @param {string} sessionId The ID of the session
|
||||
* @param {*} txn An active transaction. See doTxn(). (optional)
|
||||
*/
|
||||
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
|
||||
this._backend.addSharedHistoryInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared-history group session for a room.
|
||||
* @param {string} roomId The room that the key belongs to
|
||||
* @param {*} txn An active transaction. See doTxn(). (optional)
|
||||
* @returns {Promise} Resolves to an array of [senderKey, sessionId]
|
||||
*/
|
||||
getSharedHistoryInboundGroupSessions(roomId, txn) {
|
||||
return this._backend.getSharedHistoryInboundGroupSessions(roomId, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a transaction on the crypto store. Any store methods
|
||||
* that require a transaction (txn) object to be passed in may
|
||||
@@ -596,6 +619,7 @@ export class IndexedDBCryptoStore {
|
||||
* @param {function(*)} func Function called with the
|
||||
* transaction object: an opaque object that should be passed
|
||||
* to store functions.
|
||||
* @param {Logger} [log] A possibly customised log
|
||||
* @return {Promise} Promise that resolves with the result of the `func`
|
||||
* when the transaction is complete. If the backend is
|
||||
* async (ie. the indexeddb backend) any of the callback
|
||||
@@ -603,8 +627,8 @@ export class IndexedDBCryptoStore {
|
||||
* reject with that exception. On synchronous backends, the
|
||||
* exception will propagate to the caller of the getFoo method.
|
||||
*/
|
||||
doTxn(mode, stores, func) {
|
||||
return this._backend.doTxn(mode, stores, func);
|
||||
doTxn(mode, stores, func, log) {
|
||||
return this._backend.doTxn(mode, stores, func, log);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -613,6 +637,8 @@ IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
|
||||
= 'inbound_group_sessions_withheld';
|
||||
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS
|
||||
= 'shared_history_inbound_group_sessions';
|
||||
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
|
||||
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
|
||||
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user