Compare commits

...

1343 Commits

Author SHA1 Message Date
David Baker 6aff3ed407 v0.12.0-rc.1 2018-10-11 14:42:29 +01:00
David Baker c0ae78ae82 Prepare changelog for v0.12.0-rc.1 2018-10-11 14:42:29 +01:00
David Baker 8b22f01ecd Merge pull request #760 from matrix-org/bwindels/fixleavesinmemberlist
never replace /sync'ed memberships with OOB ones
2018-10-11 14:01:38 +01:00
Bruno Windels 2ed694b041 remove supersedes OOB logic 2018-10-11 14:32:03 +02:00
Bruno Windels a0ef6ab811 typo 2018-10-11 14:14:42 +02:00
Bruno Windels d098b39024 never replace /sync'ed memberships with OOB ones 2018-10-11 14:11:40 +02:00
David Baker 3cf23f8a5c Document breaking change 2018-10-11 12:13:25 +01:00
Bruno Windels 72e77d237a Merge pull request #759 from matrix-org/dbkr/lazy_load_check_before_sync
Don't fail to start up if lazy load check fails
2018-10-10 18:28:47 +02:00
David Baker ecc3e18e85 typo 2018-10-10 17:27:06 +01:00
David Baker dea70af889 remove debug logging 2018-10-10 17:06:26 +01:00
David Baker 30362091e5 Don't fail to start up if lazy load check fails
Do the lazy loading check in the batch of things we do before
starting a sync rather than at client start time, so we don't fail
to start the client if we can't hit the HS to determine LL support.

Fixes https://github.com/vector-im/riot-web/issues/7455
2018-10-10 16:59:36 +01:00
David Baker 0aa3362671 Merge pull request #754 from matrix-org/dbkr/e2e_on_interslice
Make e2e work on Edge
2018-10-09 10:48:24 +01:00
David Baker 92fbf58b13 Merge pull request #758 from matrix-org/bwindels/betteridberrors
throw error with same name and message over idb worker boundary
2018-10-09 09:59:55 +01:00
Bruno Windels a4b2cc84c7 Merge pull request #755 from matrix-org/travis/fix-vuln-warning
Default to a room version of 1 when there is no room create event
2018-10-08 09:55:10 +02:00
David Baker 1395da694e Merge pull request #757 from matrix-org/dbkr/gone_away_is_the_bluebird
Silence bluebird warnings
2018-10-05 13:25:16 +01:00
David Baker 264b20535e Silence bluebird warnings 2018-10-05 12:13:05 +01:00
Bruno Windels caba350b33 throw error with same name and message over idb worker boundary
instead of string currently thrown. This allows handling error
from the main thread.
2018-10-05 12:48:12 +02:00
Travis Ralston d9fe194111 Default to a room version of 1 when there is no room create event
Fixes https://github.com/vector-im/riot-web/issues/7331

There is something to be worried about when there is no room create event, however. This should always be available, although due to cache problems or servers that don't provide the event we can't be sure of this.
2018-10-04 13:42:07 -06:00
David Baker 40dc13b2e2 lint try 2 2018-10-04 15:38:08 +01:00
David Baker 4cda54ca1c lint 2018-10-04 15:15:30 +01:00
David Baker 8116c5b3f7 Make e2e work on Edge
We were sucessfully opening indexeddb but any queries using compound
indicies were failing because Edge doesn't support them, so messages
were failing to decrypt with 'DataError'.

Try a dummy query at startup, so if it fails we fall back to a
different store (ie. end up using localstorage on Edge).
2018-10-04 13:49:32 +01:00
Bruno Windels 1dcc5127d0 Merge pull request #750 from matrix-org/bwindels/allownonffmerge
allow non-ff merge from release branch into master
2018-10-04 10:22:02 +02:00
David Baker 85e3d7083c Merge pull request #751 from matrix-org/dbkr/indexeddb_errors
Reject with the actual error on indexeddb error
2018-10-03 16:22:26 +01:00
David Baker bd2da08c4e Reject with the actual error on indexeddb error
Rather than the event
2018-10-02 16:48:27 +01:00
Bruno Windels 0a88d419c6 allow non-ff merge from release branch into master 2018-10-01 17:21:01 +02:00
Bruno Windels 80c190db36 Merge branch 'master' into develop 2018-10-01 15:40:16 +02:00
Bruno Windels 550cf00ee7 Merge branch 'release-v0.11.1' 2018-10-01 15:36:37 +02:00
Bruno Windels 1e1358fcef v0.11.1 2018-10-01 15:23:37 +02:00
Bruno Windels fd1b3329f5 Prepare changelog for v0.11.1 2018-10-01 15:23:37 +02:00
Bruno Windels 9c9d8468a5 Merge pull request #744 from matrix-org/dbkr/update_mocha
Update mocha to v5
2018-09-28 12:57:49 +01:00
David Baker 6f59d62e5c Merge pull request #748 from matrix-org/bwindels/nollforguests
disable lazy loading for guests as they cant create filters
2018-09-27 19:29:54 +01:00
Bruno Windels 1c348f0cdb disable lazy loading for guests as they cant create filters 2018-09-27 18:55:21 +01:00
Bruno Windels 634596257d v0.11.1-rc.1 2018-09-27 11:47:28 +01:00
Bruno Windels 5e4973a1dc changelog 2018-09-27 11:45:33 +01:00
Bruno Windels 19f023e0ee Revert "v0.11.1-rc.1"
This reverts commit 090c15fe19.
2018-09-27 11:42:19 +01:00
Bruno Windels 090c15fe19 v0.11.1-rc.1 2018-09-27 11:38:25 +01:00
Bruno Windels e8b307dc4f Prepare changelog for v0.11.1-rc.1 2018-09-27 11:38:25 +01:00
Bruno Windels 056479d450 Revert "v0.11.1-rc.1"
This reverts commit 847d40e567.
2018-09-27 11:35:52 +01:00
Bruno Windels e5ebe2f888 Merge pull request #747 from matrix-org/bwindels/releasehubversion
make usage of hub compatible with latest version (2.5)
2018-09-27 11:30:17 +01:00
Bruno Windels e8e1b431ad make usage of hub compatible with latest version (2.5) 2018-09-27 11:28:01 +01:00
Bruno Windels 847d40e567 v0.11.1-rc.1 2018-09-27 11:20:21 +01:00
Bruno Windels cf6c555e6a Prepare changelog for v0.11.1-rc.1 2018-09-27 11:20:21 +01:00
Bruno Windels b508aa9ebc Merge pull request #746 from matrix-org/bwindels/resynconlltoggle
Detect when lazy loading has been toggled in client.startClient
2018-09-27 09:49:01 +01:00
Bruno Windels 5e7634506e Merge branch 'develop' into bwindels/resynconlltoggle 2018-09-26 18:23:07 +01:00
Bruno Windels ba39b64ced re-enable test 2018-09-26 18:00:40 +01:00
Travis Ralston fcebe89f6c Merge branch 'hs/upload-limits' into develop 2018-09-26 09:14:12 -06:00
Bruno Windels 2d5eb920b8 pass lazy loading flag into error, to format message based on it 2018-09-26 16:12:30 +01:00
Travis Ralston 26de2c86ed Merge pull request #745 from matrix-org/revert-644-hs/upload-limits
Revert "Add getMediaLimits to client"
2018-09-26 09:12:03 -06:00
Travis Ralston cba1e95d0a Revert "Add getMediaLimits to client" 2018-09-26 09:11:28 -06:00
Bruno Windels 78a5a88638 fix jsdoc lint and better naming 2018-09-26 12:49:33 +01:00
Bruno Windels b7b9c67259 fix lint 2018-09-26 12:49:26 +01:00
Bruno Windels 54bff81470 clear sync data on toggling LL,also throw spec. error and delegate clear
the sync data needs to be cleared toggling in both directions:
 not LL -> LL: you want to get rid of all the excess state in the
               sync data to get the RAM benefits
 LL -> not LL: you want to fill the sync data state again
               because getOutOfBandMembers won't be called
2018-09-26 12:39:56 +01:00
David Baker fe21972f4a Update mocha to v5
Mostly to get the non-vulnerable version of node-growl
2018-09-26 11:36:06 +01:00
Bruno Windels 58e3c72446 only store serializable options (string, boolean, number) 2018-09-26 11:34:58 +01:00
Bruno Windels 6dd5c6c317 fix existing missing this 2018-09-26 11:33:19 +01:00
Bruno Windels 4e0af3eafe don't return the IDBEvent from storeClientOptions
as it's not needed and not cloneable
2018-09-26 11:32:43 +01:00
Bruno Windels 1d0791142c all store methods should return a promise 2018-09-26 11:32:11 +01:00
Bruno Windels 2560ba2980 dont clear the store if its a brand new one 2018-09-26 10:37:52 +01:00
Bruno Windels 19be3dd852 fix lint 2018-09-26 10:13:40 +01:00
Travis Ralston 40f2a6978b Merge pull request #644 from Half-Shot/hs/upload-limits
Add getMediaLimits to client
2018-09-25 18:12:55 -06:00
Bruno Windels 1fd8c43d94 fix tests 2018-09-25 18:50:09 +01:00
Matthew Hodgson 3e32f47903 Merge pull request #742 from matrix-org/travis/build-process
Split npm start into an init and watch script
2018-09-25 17:31:23 +01:00
Travis Ralston 8f2824186a Split npm start into an init and watch script
This is to better support riot-web's build process without losing the functionality supplied by `npm start`. The watch script no longer performs an initial build and thus `start:init` has been created for this purpose.
2018-09-25 10:09:58 -06:00
Bruno Windels b0dbb20e22 fixup of in memory stores 2018-09-25 15:53:40 +01:00
Bruno Windels 0519c4c6b1 await startClient and use promises also so error gets shown 2018-09-25 15:53:14 +01:00
Bruno Windels 28184b4a29 check if lazy loading was enabled before in startClient 2018-09-25 15:32:10 +01:00
Bruno Windels 76175abea2 allow storing client options in indexeddb
so we can tell what options the sync data was created with
2018-09-25 15:30:35 +01:00
Bruno Windels 184c3dce15 Merge pull request #738 from matrix-org/revert-733-bwindels/roomnamealias
Revert "room name should only take canonical alias into account"
2018-09-20 12:23:38 +02:00
David Baker a08a3078da Revert "room name should only take canonical alias into account" 2018-09-20 11:20:49 +01:00
David Baker c2100d7622 Merge pull request #737 from matrix-org/bwindels/fixnamedisambiguation
fix display name disambiguation with LL
2018-09-20 10:22:38 +01:00
Bruno Windels a91fa59174 fix display name disambiguation with LL 2018-09-19 18:14:18 +02:00
Bruno Windels 574a6b68ae Merge pull request #735 from matrix-org/bwindels/fixstalerr
Introduce Room.myMembership event
2018-09-19 13:35:11 +02:00
Bruno Windels fc3a00054f add test for new event 2018-09-17 19:33:36 +02:00
Bruno Windels 84e41c2ade fix tests 2018-09-17 18:28:07 +02:00
Bruno Windels 4630733b55 don't fall back anymore to member, as this is more reliable 2018-09-17 18:23:48 +02:00
Bruno Windels eb690e14e4 introduce Room.myMembership event
As you don't always have your own member with lazy loading
of members enabled, looking at the sync response section
where a room appears is the most reliable way of determining
the syncing user's membership in a room.

Before we already used this to read the
current room membership with `room.getMyMembership()`,
but we were still using the `RoomMember.membership` event
to detect when the syncing user's membership changed.

This event will help make those checks work well with LL enabled.
2018-09-17 18:20:49 +02:00
David Baker 833002f846 Merge pull request #733 from matrix-org/bwindels/roomnamealias
room name should only take canonical alias into account
2018-09-14 11:38:26 +01:00
Bruno Windels 907e9fc476 fix lint 2018-09-13 10:04:31 +02:00
Bruno Windels b829a39cd2 fix tests 2018-09-13 09:59:20 +02:00
Bruno Windels daa7af0605 room name should only take canonical alias into account 2018-09-13 09:52:21 +02:00
David Baker 47a1adc864 Merge pull request #732 from matrix-org/bwindels/fixcontextstatenotwrapped
state events from context response were not wrapped in a MatrixEvent
2018-09-11 13:27:03 +01:00
Bruno Windels 98e448acdd state events from context response were not wrapped in a MatrixEvent 2018-09-11 14:13:35 +02:00
David Baker cd5cda916f Merge branch 'master' into develop 2018-09-10 11:35:25 +01:00
David Baker 33a1139772 v0.11.0 2018-09-10 11:33:51 +01:00
David Baker c5b62903f3 Prepare changelog for v0.11.0 2018-09-10 11:33:50 +01:00
David Baker 387fd16b40 v0.11.0-rc.1 2018-09-07 14:28:45 +01:00
David Baker c91b67d370 Prepare changelog for v0.11.0-rc.1 2018-09-07 14:28:44 +01:00
David Baker b809d1c263 Merge pull request #724 from matrix-org/bwindels/fastermemberinsert
Reduce amount of promises created when inserting members
2018-09-07 13:57:44 +01:00
David Baker 7bb6675abf Merge pull request #726 from matrix-org/bwindels/dontwaitforstorage
dont wait for LL members to be stored to resolve the members
2018-09-07 13:57:20 +01:00
David Baker b91bea94f4 Merge pull request #728 from matrix-org/bwindels/wrongemitargs
RoomState.members emitted with wrong argument order for OOB members
2018-09-07 13:57:09 +01:00
David Baker 96e21700bd Merge pull request #727 from matrix-org/dbkr/conn_did_fail
Only emit CATCHUP if recovering from conn error
2018-09-07 13:31:17 +01:00
David Baker 7e8f25bce3 Include 404 in connectivity success error codes 2018-09-07 13:18:45 +01:00
Bruno Windels 9e02049b05 RoomState.members emitted with wrong argument order for OOB members 2018-09-07 14:11:27 +02:00
David Baker affdfccd60 Only emit CATCHUP if recovering from conn error
Have the keepalive promise return a boolean indicating whether it
detected a connectivity failure or not. Use this to only emit
CATCHUP if there was a connectivity error, to try & suppress
the state flip-flopping back & forth between CATCHUP and ERROR
in the case where we have connectivity but the sync is returning
and error for whatever reason.
2018-09-07 11:29:44 +01:00
David Baker 402f8c27a9 Merge pull request #725 from matrix-org/dbkr/fix_sync_error_doc
Fix docstring for sync data.error
2018-09-07 09:44:28 +01:00
Bruno Windels ba4dc6c60a dont wait for LL members to be stored to resolve the members
this can easily add up to 100ms / 1000 of members
2018-09-07 10:44:06 +02:00
David Baker 6b8dd42547 Fox docstring for sync data.error
It's 'error', not 'err'
2018-09-06 19:53:52 +01:00
Bruno Windels 1511a27f4c update/remove comments 2018-09-06 18:09:46 +02:00
Bruno Windels 7ae6c147fa lint doesnt like async 2018-09-06 18:02:19 +02:00
Bruno Windels f51630eb07 dont create a promise for every inserted member but await the transaction instead 2018-09-06 18:01:20 +02:00
Bruno Windels e3586411e0 Merge pull request #723 from matrix-org/revert-721-revert-717-bwindels/fixllroompermission
Re-apply "Don't rely on members to query if syncing user can post to room"
2018-09-06 14:55:24 +02:00
David Baker a0639a32c7 Revert "Revert "Don't rely on members to query if syncing user can post to room"" 2018-09-06 11:47:40 +01:00
David Baker 759c6e77a7 Merge pull request #721 from matrix-org/revert-717-bwindels/fixllroompermission
Revert "Don't rely on members to query if syncing user can post to room"
2018-09-05 18:09:59 +01:00
David Baker 04ad3d7c3c Revert "Don't rely on members to query if syncing user can post to room" 2018-09-05 18:04:19 +01:00
Bruno Windels 49badd9a2f Merge pull request #717 from matrix-org/bwindels/fixllroompermission
Don't rely on members to query if syncing user can post to room
2018-09-05 13:07:29 +02:00
Bruno Windels 8b00083bca check power levels without relying on membership
as this might not be known for the syncing user.
instead, add a method to room which always knows the syncing user's membership
2018-09-05 12:36:53 +02:00
Bruno Windels 3d98e324b5 Merge pull request #719 from matrix-org/bwindels/fixavatars-parttrois
Fixes for room.guessDMUserId
2018-09-04 18:17:39 +02:00
Bruno Windels 768c66313f remove unneeded async keywords 2018-09-04 18:09:47 +02:00
Bruno Windels 3561fd1c05 Merge pull request #716 from matrix-org/bwindels/fixfilepanel
Fix filepanel also filtering main timeline with LL turned on.
2018-09-04 17:10:49 +02:00
Bruno Windels a6c055b6d1 Merge pull request #711 from matrix-org/bwindels/clearidbmembersonleave
Remove lazy loaded members when leaving room
2018-09-04 16:53:56 +02:00
Bruno Windels 0d24c18fed add comment to explain fix 2018-09-04 16:51:36 +02:00
Bruno Windels dd8b2a79fb Merge pull request #702 from matrix-org/bwindels/fixreconnectspinner
Fix: show spinner again while recovering from connection error
2018-09-04 16:39:38 +02:00
Bruno Windels f0095611bc add new CATCHUP state as breaking change 2018-09-04 16:38:46 +02:00
Bruno Windels a3567f0918 some tests for room.guessDMUserId() 2018-09-04 13:01:45 +02:00
Bruno Windels f0d3d0d74e remove unneeded checks, we should always have enough heroes or members 2018-09-04 13:01:15 +02:00
Bruno Windels 3e32bc0d5d check heroes is not falsy first, this would fail without LL 2018-09-04 13:00:34 +02:00
Bruno Windels 632e4aa120 pick the first member, dont need an array 2018-09-04 13:00:00 +02:00
Bruno Windels 2391ce198d this method returns userId, not member 2018-09-04 12:59:37 +02:00
David Baker e5e2bbd482 Merge branch 'master' into develop 2018-09-03 14:11:12 +01:00
David Baker 0b6632123b v0.10.9 2018-09-03 14:09:24 +01:00
David Baker b1801fc953 Prepare changelog for v0.10.9 2018-09-03 14:09:24 +01:00
Bruno Windels ca1a1c4f28 Merge pull request #714 from matrix-org/bwindels/memberlist-spinner
Add method to query LL state in client
2018-09-03 15:05:13 +02:00
Bruno Windels 3363cc4f1d shallow-clone the filter, so the timeline filter doesnt get written into it later on 2018-09-03 15:02:38 +02:00
Bruno Windels f84684982f add method to query LL state in client 2018-09-03 11:14:23 +02:00
Bruno Windels 3bed5969bf remove count logging, approach confirmed to work and be according to idb spec 2018-09-03 10:27:00 +02:00
Bruno Windels ebc162e3d8 do onLeft (which clears the LL members) as late as possible
to avoid chance that something might call loadMembersIfNeeded
on the room and load them back again.
2018-08-31 16:13:34 +02:00
Bruno Windels f30136dba3 only clear promise at the end to avoid race between load and clear members 2018-08-31 16:12:54 +02:00
Bruno Windels 9b1926f902 also clear out lazy loaded members from storage 2018-08-31 16:11:37 +02:00
David Baker d29524ba3f Merge pull request #707 from matrix-org/bwindels/llinvites
Fix: also load invited members when lazy loading members
2018-08-31 14:09:42 +01:00
Bruno Windels 7258fe4e5c clear out of band members in store when leaving room 2018-08-31 14:42:15 +02:00
Bruno Windels f8ea1702f8 store support for removing out of band members for a room 2018-08-31 14:42:15 +02:00
David Baker 7582c28c1a v0.10.9-rc.2 2018-08-31 13:40:37 +01:00
David Baker 5042eb87e7 Prepare changelog for v0.10.9-rc.2 2018-08-31 13:40:36 +01:00
David Baker e8e80732ef Merge pull request #710 from matrix-org/bwindels/avatarfixbis
Fix for "otherMember.getAvatarUrl is not a function"
2018-08-31 13:32:25 +01:00
David Baker b8744a79ae Merge pull request #708 from matrix-org/bwindels/avatarfixbis
Fix for "otherMember.getAvatarUrl is not a function"
2018-08-31 10:34:29 +01:00
Bruno Windels 414b153d28 also fallback to getting avatar from user 2018-08-31 11:05:06 +02:00
Bruno Windels 8e160dda8e make sure getAvatarFallbackMember always returns a member 2018-08-31 11:03:26 +02:00
David Baker 9b54c9b807 Merge pull request #704 from matrix-org/dbkr/discardsession
Pass through function to discard megolm session
2018-08-30 18:25:50 +01:00
Bruno Windels 1bb608cdb6 remove filter for LL members so invite members are also sent 2018-08-30 17:29:16 +02:00
Bruno Windels 1239485b30 fix test 2018-08-30 15:42:15 +02:00
Bruno Windels 0d23d047fc use CATCHUP state after ERROR before going back to SYNCING 2018-08-30 15:37:05 +02:00
Bruno Windels d837ae64ac triple = 2018-08-30 15:37:05 +02:00
Bruno Windels d72a70396a Pass through PREPARED state after error, when keepalive returns succes.
This is according to the state diagram in client.js.
This will show a spinner at the bottom of a room again
while the catchup sync is in progress,
which seems to have broken at some point.
2018-08-30 15:37:05 +02:00
David Baker 938772b86a v0.10.9-rc.1 2018-08-30 14:16:14 +01:00
David Baker 3e88593a81 Prepare changelog for v0.10.9-rc.1 2018-08-30 14:16:14 +01:00
David Baker 60c01d7869 Revert b0b0291 and a6de395
To make tests pass again
2018-08-30 12:03:53 +01:00
David Baker 1cbcc61bd6 Merge pull request #706 from matrix-org/bwindels/fixdmavatar
Fix DM avatar
2018-08-30 11:43:07 +01:00
Bruno Windels 7f5a2974ce allow self chats 2018-08-30 12:37:13 +02:00
Bruno Windels 3de3ea38b9 check heroes is present 2018-08-30 11:47:17 +02:00
Bruno Windels 3659e86d57 fix lint, actually get members as well 2018-08-30 11:41:35 +02:00
Bruno Windels c335a6b3de guess DM user id, used to patch up incorrect m.direct account data 2018-08-30 11:05:31 +02:00
Bruno Windels 267b831bc4 calculate fallback avatar for rooms with <= members but not DM 2018-08-30 11:05:05 +02:00
Bruno Windels 7ee93cb910 make sure our user is not in the summary heroes.
No reason to think it is right now, but if there is a server bug
we could end up showing your own avatar for a DM again.

Also convenience method as we add up invited + join count often
2018-08-30 11:04:32 +02:00
Matthew Hodgson ae95a49618 spell out m.new_devices no longer exist 2018-08-29 20:20:52 +01:00
David Baker 8f98504183 jsdoc 2018-08-29 18:11:53 +01:00
David Baker 1b77ee0ef4 Pass through function to discard megolm session
To make debugging crypto slightly faster
2018-08-29 18:06:45 +01:00
Matthew Hodgson a6de395cde unbreak tests from b0b0291bc7 2018-08-28 18:03:59 +01:00
Bruno Windels fcd6dd34b2 Merge pull request #699 from matrix-org/bwindels/fixlle2ememberfetch-bis
Lazy loading: avoid loading members at initial sync for e2e rooms
2018-08-28 15:36:42 +02:00
Bruno Windels a6ebfe4215 typo 2018-08-28 15:31:20 +02:00
Bruno Windels 6a9158aa62 Merge pull request #700 from matrix-org/bwindels/fixllmegolmsession
Improve setRoomEncryption guard against multiple m.room.encryption st…
2018-08-28 15:30:16 +02:00
Matthew Hodgson b0b0291bc7 hopefully fix invite_room_state as per https://github.com/vector-im/riot-web/issues/7229 2018-08-28 01:27:20 +01:00
Bruno Windels 85f1da1f10 revert unnecesary changes 2018-08-27 12:09:10 +02:00
Bruno Windels c47445ca98 no need to just add a space now 2018-08-27 12:01:22 +02:00
Bruno Windels 4e25867548 revert to async event processing
without LL, we could refresh the device list before all members have been tracked.
as promises, even resolved ones (in case of no LL), always continue async
2018-08-27 11:56:46 +02:00
Bruno Windels ad71bb30ac add comment back as we kept flag in the end 2018-08-27 11:51:29 +02:00
Bruno Windels 362bf1895d restore inhibitDeviceQuery param to avoid breaking change 2018-08-27 11:12:00 +02:00
Bruno Windels 7d00c0bd5a make LL/non-LL flow in Crypto more alike by always going through _roomDeviceTrackingState 2018-08-27 10:54:08 +02:00
Bruno Windels 5e5994f166 try and fix tests 2018-08-27 10:54:08 +02:00
Bruno Windels 7247762b60 Also support not lazy-loading members in Crypto 2018-08-27 10:54:08 +02:00
Bruno Windels 21e0c79f7d Revert "Revert "Lazy loading: don't block on setting up room crypto""
This reverts commit 5cf2ebea4f.
2018-08-27 10:54:08 +02:00
Bruno Windels 78b08bfef2 fix var declaration 2018-08-27 10:48:11 +02:00
Bruno Windels ae7e90dc2f do config comparison first to keep original error message 2018-08-27 10:39:14 +02:00
Bruno Windels 0028bfbfc7 fix lint 2018-08-24 18:42:48 +02:00
Bruno Windels 60c9c403bd Improve setRoomEncryption guard against multiple m.room.encryption state events
we were only bailing out when receiving a non JSON-identical m.room.encryption event.
When receiving an identical event, the algorithm in _roomEncryptors would be reset,
generating a new megolm session every time this happens (there is a LL synapse bug
where this happens on every sync).

As the _roomList is backed by indexeddb you might already have a config without the algorithm being present though,
so we first check for the room encryptor algorithm being present. If so, always bail out as setRoomEncryption was
already called for the given room.

If no algorithm is present, still check if the config is not being changed.
Also setup the roomlist and room encryption synchronously before awaiting
the indexeddb operation to store the room encryption config in roomlist.
2018-08-24 18:28:38 +02:00
Bruno Windels a7199a3d0d Merge pull request #698 from matrix-org/revert-696-bwindels/fixlle2ememberfetch
Revert "Lazy loading: don't block on setting up room crypto"
2018-08-23 14:10:13 +02:00
Bruno Windels 5cf2ebea4f Revert "Lazy loading: don't block on setting up room crypto" 2018-08-23 14:05:57 +02:00
David Baker 580e95605e Merge pull request #696 from matrix-org/bwindels/fixlle2ememberfetch
Lazy loading: don't block on setting up room crypto
2018-08-23 12:41:33 +01:00
David Baker 8c3d1df3cf Merge pull request #695 from matrix-org/dbkr/hide_replaced_rooms
Add getVisibleRooms()
2018-08-23 09:34:39 +01:00
David Baker 7c66f91429 Typo 2018-08-23 09:34:23 +01:00
Bruno Windels e3d108454c fix test 2018-08-22 23:25:37 +02:00
Bruno Windels 806b40727d fix lint 2018-08-22 23:00:27 +02:00
Bruno Windels fa702efe8f fix typo 2018-08-22 19:22:50 +02:00
Bruno Windels 344e3e18ab start tracking room devices in background after finishing loading members 2018-08-22 19:13:18 +02:00
Bruno Windels aea9eaa307 Only start tracking devices in an e2e room when needed
This way we can put off loading the members
2018-08-22 18:00:38 +02:00
Bruno Windels dffe0b39b6 Merge pull request #697 from matrix-org/dbkr/joined_member_count_wrapper
Add wrapper around getJoinedMemberCount()
2018-08-22 17:44:59 +02:00
Bruno Windels 267d660527 add invite count to room as well 2018-08-22 17:42:30 +02:00
David Baker 4c3046f917 Add wrapper around getJoinedMemberCount()
On Room, because it's super confusing that Room has
getJoinedMembers() but not getJoinedMemberCount()

https://github.com/matrix-org/matrix-react-sdk/pull/2126 had assumed
that this method was on Room
2018-08-22 16:34:06 +01:00
Bruno Windels 71444b638b don't block on setting up room crypto
this will load members in case of LL and could take quite some time
The two async actions performed by onCryptoEvent is saving the crypto config
to the roomlist store and fetching the room members to track.
Both are guarded against double calls so not awaiting this should be fine.
2018-08-22 14:45:36 +02:00
David Baker 962ec7bb53 Add getVisibleRooms()
To hide rooms that have been replaced

For https://github.com/vector-im/riot-web/issues/7164
2018-08-22 11:58:44 +01:00
Bruno Windels 52fad6aec2 Merge pull request #694 from matrix-org/hs/fetch-room-event-api
Api to fetch events via /room/.../event/..
2018-08-21 14:39:14 +02:00
Will Hunt 5b830f0b6a Add fetchRoomEvent to base-apis.js 2018-08-21 11:07:48 +01:00
Will Hunt 3d24c8768f Drop fetchRoomEvent from client.js 2018-08-21 11:07:37 +01:00
Bruno Windels 977b8625f8 Merge pull request #693 from matrix-org/dbkr/room_upgrades
Support for room upgrades
2018-08-21 11:22:08 +02:00
Will Hunt df7dc04a1d Happy linter, happy developer 2018-08-20 14:16:47 +01:00
David Baker 269d3cb086 Merge branch 'master' into develop 2018-08-20 13:49:20 +01:00
David Baker 4d310cd461 v0.10.8 2018-08-20 13:44:30 +01:00
David Baker 88c5c39fcb Prepare changelog for v0.10.8 2018-08-20 13:44:29 +01:00
Will Hunt 79ca68300c Add callback 2018-08-20 12:47:26 +01:00
Will Hunt baca20b225 Add support for /rooms/$roomId/event/$eventId 2018-08-20 11:14:45 +01:00
David Baker 0e3cb1977f lint 2018-08-17 15:00:54 +01:00
David Baker 8b1fa72877 Copyright & debadgering 2018-08-17 14:59:34 +01:00
David Baker e8610a35b4 Support for room upgrades
For https://github.com/vector-im/riot-web/issues/7164
2018-08-17 14:55:12 +01:00
Bruno Windels 77e6442f73 Merge pull request #691 from matrix-org/bwindels/feature_lazyloading
Lazy loading of room members
2018-08-16 18:14:33 +02:00
David Baker eeddfd4919 v0.10.8-rc.1 2018-08-16 15:17:36 +01:00
David Baker fa16da86b3 Prepare changelog for v0.10.8-rc.1 2018-08-16 15:17:35 +01:00
Bruno Windels 372a628cab fix log whitespace 2018-08-15 12:01:26 +02:00
Bruno Windels 2f4d8c3530 check with server if it supports member lazy loading 2018-08-15 12:01:26 +02:00
Bruno Windels 482eab0e2a fix tests 2018-08-15 12:01:26 +02:00
Bruno Windels 03c63d9b12 use sync token for /members request, as synapse expects it now 2018-08-15 12:01:26 +02:00
Bruno Windels 07e87915ba fix and add tests 2018-08-15 12:01:26 +02:00
Bruno Windels 91f2bf99c0 fix lint 2018-08-15 12:01:26 +02:00
Bruno Windels 535d59db4d fixup 2018-08-15 12:01:26 +02:00
Bruno Windels 9739c3355a undo postponing tracking device keys on turning on room encryption 2018-08-15 12:01:26 +02:00
Bruno Windels 827db37eef fixup 2018-08-15 12:01:26 +02:00
Bruno Windels 0c6e47a5bc await for LL members in getEncryptionTargetUsers 2018-08-15 12:01:26 +02:00
Bruno Windels 864ea749e5 Move /members fetching to room as getEncryptionTargetMembers needs it 2018-08-15 12:01:26 +02:00
Bruno Windels 5d92ec3b7b prevent deadlock on startup
when loading the encrypted events from storage,
the code would wait for the encryption target users,
which would never come because you would only load them
when viewing the room.

This disabled starting to track the devices in the room
when the inhibitDeviceQuery is set.
2018-08-15 12:01:26 +02:00
Bruno Windels 733a3ed102 Defer encryption targeted users when OOB member loading hasn't started 2018-08-15 12:01:26 +02:00
Bruno Windels b14be026b7 pass LL flag to room, to know if we should wait for lazy members at all 2018-08-15 12:01:26 +02:00
Bruno Windels b4afe97289 spelling 2018-08-15 12:01:26 +02:00
Bruno Windels 43a7a607b2 fixup 2018-08-15 12:01:26 +02:00
Bruno Windels a8bf66d8af Make Room.getEncryptionTargetMembers async, as members might be loading 2018-08-15 12:01:26 +02:00
Bruno Windels 3616a07dbb store /members promise on room while loading members 2018-08-15 12:01:26 +02:00
Bruno Windels 6609dfd410 initial support for lazy loading when calling /context 2018-08-15 12:01:26 +02:00
Bruno Windels e3913bd397 Fix: missed call site while renaming prependStateEvents 2018-08-15 12:01:26 +02:00
Bruno Windels 8c01ed1469 add comments explaining why we ignore the put promise result 2018-08-15 12:01:26 +02:00
Bruno Windels 7aa0dcc89f PR feedback, rename method 2018-08-15 12:01:26 +02:00
Bruno Windels 5285b22a76 bump version as making startClient async is a breaking change 2018-08-15 12:01:26 +02:00
Bruno Windels 0fa49bc2cd PR feedback 2018-08-15 12:00:38 +02:00
Bruno Windels 01d8730850 cleanup, lint and docs 2018-08-15 12:00:38 +02:00
Bruno Windels 52149ce74a Move LL filter creation inside MatrixClient
As we need an option to turn lazy loading on (we can't just accept a filter,
as /messages has an incompatible filter), better only pass the option
and create the filter inside startClient
2018-08-15 12:00:38 +02:00
Bruno Windels bffc20612d Fix: member avatar was always forward looking
applying itself all the way till the next member event
when back paginating
2018-08-15 12:00:38 +02:00
Bruno Windels 2c0eb19a27 Fix sentinels changing from underneath us!
As RoomMember contains the event in a nested object (events.member),
a shallow copy was not enough to be immutable.

This solution won't copy OOB flags but that's not neccesary
for sentinels.
2018-08-15 12:00:38 +02:00
Bruno Windels 748c4737f6 remove dead code 2018-08-15 12:00:38 +02:00
Bruno Windels 769d5113f7 prepend state from /messages to appropriate timeline/room state (excluding event context for now) 2018-08-15 12:00:38 +02:00
Bruno Windels 157be6da05 centralize creating a /messages request
so we only need to add LL filter once
2018-08-15 12:00:38 +02:00
Bruno Windels 1dc4b8bb63 add option for lazy loading to startClient
we need more than just a filter, which is what is passed in now,
so have an explicit option. For now still take the filter but later on
this could be created inside MatrixClient
2018-08-15 12:00:38 +02:00
Bruno Windels f261599435 fix lint 2018-08-15 12:00:38 +02:00
Bruno Windels 2862b49057 Only return hero in getDMInviter if we were invited 2018-08-15 12:00:38 +02:00
Bruno Windels a8d0d8f33d provide method on room to help with DM detection with fallback to summary heroes/counts. 2018-08-15 12:00:38 +02:00
Bruno Windels f55a2079bf replace getMember(myId).membership with getMyMembership
This works with rooms which haven't had their members
loaded yet.
2018-08-15 12:00:38 +02:00
Bruno Windels c0f706a2a2 move userId into room 2018-08-15 12:00:38 +02:00
Bruno Windels b034f67a0f add oob member methods to stub store 2018-08-15 12:00:38 +02:00
Bruno Windels 977b9eb686 implement memory store methods 2018-08-15 12:00:38 +02:00
Bruno Windels 5e11bf735e store OOB status along with members, to avoid unneccesary fetching
for some small rooms, it is possible that calling /members would not
yield any previously unknown members, as they were all recently active.
This would be the case for most DMs.

For these rooms, we'd end up with 0 OOB members after lazy loading them,
so when getting them out of storage we need a way to distuinguist this case
from never having lazy loaded the members of the room at all.

We store a marker object in the same store and return [] or null accordingly.
This way the /members don't get fetched a second time.
2018-08-15 12:00:38 +02:00
Bruno Windels a8c73f7a4d add logging, should be useful as long as not merged into develop 2018-08-15 12:00:38 +02:00
Bruno Windels 86105611fc we dont need a separate index? 2018-08-15 12:00:38 +02:00
Bruno Windels 0364af7337 update indexeddb store to store member events, not profile information 2018-08-15 12:00:38 +02:00
Bruno Windels c618ce4625 store only out of band members 2018-08-15 12:00:38 +02:00
Bruno Windels 2b9c834476 add comment to clarify how we avoid race 2018-08-15 12:00:38 +02:00
Bruno Windels d366ec9c48 prototype how we could store ll members 2018-08-15 12:00:38 +02:00
Bruno Windels ca3981fba8 back-port infinite spinner fix as room state code changed considerably for lazy loading 2018-08-15 12:00:38 +02:00
Bruno Windels bb490faefe fix lint 2018-08-15 11:59:40 +02:00
Bruno Windels d8f673ed51 make sure invited count cache gets reset when updating member 2018-08-15 11:59:40 +02:00
Bruno Windels 6ce7170cf4 counts from summary api should override count members manually as members might not be complete 2018-08-15 11:59:40 +02:00
Bruno Windels 1d71e7243f no need to create new array here 2018-08-15 11:59:40 +02:00
Bruno Windels cf08901d02 fix lint 2018-08-15 11:59:40 +02:00
Bruno Windels 230a9311a0 actually need to subtract one from join+invite count as that includes the syncing user 2018-08-15 11:59:40 +02:00
Bruno Windels 576f7142c1 just need the member names actually 2018-08-15 11:59:40 +02:00
Bruno Windels 20b4285849 add some tests for room name based on room summary + fix because it was actually broken 2018-08-15 11:59:40 +02:00
Bruno Windels d67bdbf088 test accumulating summary fields 2018-08-15 11:59:40 +02:00
Bruno Windels 3a389793ff fix sync accumulator test to include summary 2018-08-15 11:59:40 +02:00
Bruno Windels f5ff5dc3e0 Fix name recalculation tests by not relying on mocking
I tried keeping the mocking but it would take too much
boilerplate code to make the tests work again, and even more
to write the tests for room name with lazy loading.

Just testing everything with a real implementation is not really
a unit test any more, but proved way easier.

It'll be somewhat annoying these tests will fail if there is
something wrong in roomstate (not room), but that's the trade-off
2018-08-15 11:59:40 +02:00
Bruno Windels 00bf5bdf69 unify member a bit access towards getMember
some tests for mock getMember, some for .members
if you use either in the code (as I did for room display name changes)
tests start playing and you play whack-a-mole switching between
both ways of accessing the members in a room.

lets start using one way so mocking becomes easier,
and besides, accessing an object internal members is not the best idea.
2018-08-15 11:59:40 +02:00
Bruno Windels 9541aa7dbf fix lint 2018-08-15 11:59:40 +02:00
Bruno Windels e61c6b89c8 bring room name calculation in line with summary spec, while maintaining some backwards compatibility 2018-08-15 11:59:40 +02:00
Bruno Windels a5b3869e9f add invited count, only copy summary fields if present in summary
only copy any member from summary as
they are only in the response when they change.
Also accumulate them in the sync accumulator
2018-08-15 11:59:40 +02:00
Bruno Windels fbdce27db2 m.heros => m.heroes 2018-08-15 11:59:40 +02:00
Bruno Windels 148876f597 sorting should happen always 2018-08-15 11:59:40 +02:00
Bruno Windels 0cb533beca no need to recalculate name here, as recalculate already does this 2018-08-15 11:59:40 +02:00
Bruno Windels 5811ebd6f3 Support summary heroes in room name calculation
Also clean-up algorithm, and remove assumption
that we have all members as much as possible
2018-08-15 11:59:40 +02:00
Bruno Windels 8fa87f8ba5 make room summary available to Room
from either the sync accumulator or the /sync endpoint
2018-08-15 11:59:40 +02:00
Bruno Windels 21ba4f71f6 jsdoc doesn't like generic promise type annotations 2018-08-15 11:59:40 +02:00
Bruno Windels 097e7df7c9 fix lint 2018-08-15 11:59:40 +02:00
Bruno Windels 83c6615d6e move me || syncedmembership code into room.getmymembership 2018-08-15 11:59:40 +02:00
Bruno Windels f6fafeaafb store membership from during sync, because we might not have own membership
to determine where a room should show up in the room list, we need to know
our membership type. But with lazy loading, we might not have our own member
if we weren't recently active in the room. Using getSyncedMembership can be
used to fallback if the users membership is not yet available.
2018-08-15 11:59:40 +02:00
Bruno Windels 420a88c776 remove obsolete flag 2018-08-15 11:59:40 +02:00
Bruno Windels 5fcf9481b3 fix room not having access to event mapper + tests 2018-08-15 11:59:39 +02:00
Bruno Windels 48c3dcc08a fix lint & fix and add tests 2018-08-15 11:59:39 +02:00
Bruno Windels 62333b3e2c Use /members api for lazy loading
This commit is a substantial change, as /members returns state events,
not profile information as /joined_members, and this allows to simplify
the implementation quite a bit. We can assume again all members have
a state event associated with it.

I also changed most of the naming of lazy loaded members to
out-of-band members to reflect that this is the relevant bit for most
of the code, that the members didn't come through /sync but through
another channel.

This commit also addresses the race condition between /(joined_)members
and /sync. /members returns the members at the point in the timeline
at a given event id. Members are loaded at the last event
in the live timeline, and all members that come in from sync
in the mean time are marked as superseding the out of band members,
so they won't be overwritten, even if the timeline is reset in the
mean time.

Members are also marked if they originate from an out-of-band channel
(/members) so they can be stored accordingly (future PR).

The loading status is kept in room state now, as this made resolving
the race condition easier. One consequence is that the status needs
to be shared across cloned instances of RoomState. When resetting
the timeline (and cloning the room state) while lazy loading is in
progress, one of the RoomStates could be left in progress indefinitely.
Though that is more for clarity than avoiding any actual bugs.
2018-08-15 11:59:39 +02:00
Bruno Windels df758b31b7 fix lint 2018-08-15 11:59:39 +02:00
Bruno Windels 9f08bfaa6f room lazy loading tests + fix 2018-08-15 11:59:39 +02:00
Bruno Windels 198d2c780d test that modifications to clone'd() room state dont affect the old 2018-08-15 11:59:39 +02:00
Bruno Windels ab1c0dabae make sure LL members don't needlessly get disambiguated during a clone
when cloning the state, lazy loaded members are copied over with their rawDisplayName,
which could originate from their userId if they don't have a displayname.
the displayname algorithm would assume that the displayname is explicitly set,
and see if we'd have to disambiguate. As a fix, if the display name is the same as the id, just return the id
2018-08-15 11:59:39 +02:00
Bruno Windels 0234f11914 some tests for room member + state, and some fixes to make them pass 2018-08-15 11:59:39 +02:00
Bruno Windels 79fcc9f343 only set the lazy members on the forward looking state of the live timeline
since back-paginating will also support lazy loading the state needed to display that part of the timeline, and no user interaction
is supposed to happen before the lazy loaded member are, well, loaded, applying the ll members to all timelines should not be neccessary.
2018-08-15 11:59:39 +02:00
Bruno Windels 031f722540 clarify that we only get joined members for now 2018-08-15 11:59:39 +02:00
Bruno Windels 531ccf1819 actually, comment should be 1 line higher 2018-08-15 11:59:39 +02:00
Bruno Windels 0d2ac42dc4 add comment to clarify corners cut in prototype 2018-08-15 11:59:39 +02:00
Bruno Windels 9ec6ea3bdf 2, not 3 times 2018-08-15 11:59:39 +02:00
Bruno Windels 1ce580bba3 test lazy loaded info is returned and then discarded when setting a state event 2018-08-15 11:59:39 +02:00
Bruno Windels 8ad2a94a90 make sure LL member doesn't override state event
extra safety check, as this should already not happen because of the check in RoomState
2018-08-15 11:59:39 +02:00
Bruno Windels de3f75bc83 Lazy loaded members should never take precendence over members acquired through state events 2018-08-15 11:59:39 +02:00
Bruno Windels 008d85ed32 pick joined property out of response 2018-08-15 11:59:39 +02:00
Bruno Windels 5e30aff418 more consistent naming 2018-08-15 11:59:39 +02:00
Bruno Windels 5de0d39553 move the fact that we're prototyping only with joined members up the stack to client
only MatrixClient really needs to know that for now we only load joined members, the rest of the code can be generic for other membership types as that is the eventual plan, to also support invites at least.
2018-08-15 11:59:39 +02:00
Bruno Windels d95d44dc94 move error handling to caller 2018-08-15 11:59:39 +02:00
Bruno Windels 6061deac37 use method for getting state event, less code 2018-08-15 11:59:39 +02:00
Bruno Windels ba34a766e7 fix lint 2018-08-15 11:59:39 +02:00
Bruno Windels 1c81a17298 Fix tests
getSentinelMember now does return a member (with just the userid) when there is no corresponding member yet.
With lazy loading it's perfectly possible the member is not available, and null breaks continuation in the timeline.
2018-08-15 11:59:39 +02:00
Bruno Windels 2097b31d4f handle failed /joined_members call by logging to console and reverting flag 2018-08-15 11:59:39 +02:00
Bruno Windels 2155dd0552 improve return type comment 2018-08-15 11:59:39 +02:00
Bruno Windels 8733654094 remove left-over test code, oops 2018-08-15 11:59:39 +02:00
Bruno Windels abd15748ce fix one lint warning too many 2018-08-15 11:59:39 +02:00
Bruno Windels 9a796f1383 fix lint errors 2018-08-15 11:59:39 +02:00
Bruno Windels 88f2f62945 make resetting the live timeline work with lazily loaded members
In order for the lazy loading logic not to bleed into all corners
of the JS SDK, I moved some of the state copying between timelines
over to the RoomState and EventTimeLine class.
2018-08-15 11:59:39 +02:00
Bruno Windels 30adefed07 return sentinels with userid if members haven't been loaded yet,
better than braking timeline continuation
2018-08-15 11:59:39 +02:00
Bruno Windels 20a1828fa5 make sentinels lazy loading compatible
dont just rely on member events, but just copy the member
2018-08-15 11:59:39 +02:00
Bruno Windels 809674ca2b set lazily loaded members on all RoomStates of a room
for all timelines in all timeline sets
2018-08-15 11:59:39 +02:00
Bruno Windels 0ca3475878 make method to get mxc avatar url public
so MemberInfo can use it and take lazy loading into account
2018-08-15 11:59:39 +02:00
Bruno Windels 32b741e205 use more consistent naming 2018-08-15 11:59:39 +02:00
Bruno Windels c917c4a468 return correct invite sender in case of a join 2018-08-15 11:59:39 +02:00
Bruno Windels 6c584d2b4c keep is_direct checks inside RoomMember as events.member might not be available 2018-08-15 11:59:39 +02:00
Bruno Windels 759d415d40 preserve member state event if available when lazy loading members 2018-08-15 11:59:39 +02:00
Bruno Windels 45d86fa270 emit individual events for lazily loaded members
emit individual RoomState.members/newMember events
for each lazily loaded member as batch events are not a thing.
This makes updating the memberlist work
2018-08-15 11:59:39 +02:00
Bruno Windels 2c5cad71ee prototype support for lazily loading members in matrixclient 2018-08-15 11:59:39 +02:00
Bruno Windels 2b5925b893 Support for updating members in RoomState from lazily loaded members 2018-08-15 11:59:39 +02:00
Bruno Windels f012ada2c4 add setter on RoomMember to update from lazily loaded member 2018-08-15 11:59:07 +02:00
Bruno Windels af1b26ae95 Merge pull request #689 from matrix-org/dbkr/show_room_version
Add getVersion to Room
2018-08-14 17:54:09 +02:00
David Baker f72f5b43e1 Add getVersion to Room
To get the version number of the room as per https://github.com/matrix-org/matrix-doc/issues/1425
2018-08-14 14:27:08 +01:00
Bruno Windels c7e1e07262 Merge pull request #680 from matrix-org/dbkr/getsyncstatedata
Add getSyncStateData()
2018-08-06 10:51:21 +02:00
Bruno Windels 24a1bec23d Merge pull request #679 from matrix-org/dbkr/chairman_mau_pt_3_sync_error
Send sync error to listener
2018-08-03 19:06:17 +02:00
David Baker 89ad104423 Add getSyncStateData()
To get additional information about the sync state (ie. the error
object).
2018-08-03 18:00:52 +01:00
David Baker c2f3324302 Send sync error to listener
We do this in other places, but not here
2018-08-03 16:59:10 +01:00
Michael Telatynski 04a969b997 Merge pull request #675 from matrix-org/bwindels/nocrashoninvalidtags
make sure room.tags is always a valid object to avoid crashes
2018-08-01 23:03:51 +01:00
Bruno Windels 630dfa9499 make sure room.tags is always a valid object so no crashes happen later on 2018-07-31 15:33:56 +02:00
Bruno Windels 95668950c2 Merge pull request #673 from matrix-org/bwindels/infinite_spinner
Fix infinite spinner upon joining a room
2018-07-31 14:57:04 +02:00
Bruno Windels 3012501e4b update docs to clarify state when emitting newMember 2018-07-30 14:19:45 +02:00
David Baker 0e81dfb004 v0.10.7 2018-07-30 11:40:08 +01:00
David Baker 35b7f358b6 Prepare changelog for v0.10.7 2018-07-30 11:40:08 +01:00
Bruno Windels e3e48944e0 add test 2018-07-27 11:49:55 +02:00
Bruno Windels 94bbba72f5 add member to members before emitting any events 2018-07-27 11:35:19 +02:00
David Baker c429ca67b9 v0.10.7-rc.1 2018-07-24 18:15:14 +01:00
David Baker bce2ba0785 Prepare changelog for v0.10.7-rc.1 2018-07-24 18:15:13 +01:00
Matthew Hodgson 2613690064 Merge pull request #666 from matrix-org/matthew/encrypt-for-invited-users
encrypt for invited users if history visibility allows.
2018-07-10 16:14:45 +01:00
David Baker 7283076bc8 Fix Users / Members mixup 2018-07-09 17:16:23 +01:00
David Baker f43d05b54e Merge branch 'master' into develop 2018-07-09 13:07:40 +01:00
David Baker c6b500bc09 v0.10.6 2018-07-09 13:06:07 +01:00
David Baker c3972015c7 Prepare changelog for v0.10.6 2018-07-09 13:06:06 +01:00
David Baker 3c18c57857 v0.10.6-rc.1 2018-07-06 15:40:55 +01:00
David Baker f562a06707 Prepare changelog for v0.10.6-rc.1 2018-07-06 15:40:54 +01:00
Michael Telatynski 7f50dd205f displayname disambiguation fixes (#662)
* fix displayname=undefined being disambiguated and strip Zero Width chars
* also strip diaritics and whitespace

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-07-06 10:42:53 +01:00
David Baker c4fe15400c Merge pull request #665 from matrix-org/luke/feature-expose-decryption-error-2
Expose event decryption error via Event.decrypted event
2018-07-05 11:02:26 +01:00
David Baker 6e3e8f7310 Merge pull request #663 from matrix-org/luke/feature-decryption-error-codes
Add decryption error codes to base.DecryptionError
2018-07-05 11:00:33 +01:00
Matthew Hodgson 5ae2c26130 lint 2018-07-05 02:01:10 +02:00
Matthew Hodgson d8d35f4022 encrypt for invited users if history visibility allows.
fixes https://github.com/vector-im/riot-web/issues/2713
2018-07-05 01:45:45 +02:00
Luke Barnard fadb4d9219 Send OLM_UNKNOWN_MESSAGE_INDEX when possible 2018-07-04 15:58:45 +01:00
Luke Barnard b63149b36a Fix error code for Olm group message decryption 2018-07-04 14:33:02 +01:00
Luke Barnard 79f92abcfa Add jsdoc for Event.decrypted error 2018-07-04 14:28:15 +01:00
Luke Barnard 0137fb468b Expose event decryption error via Event.decrypted event 2018-07-04 13:56:03 +01:00
Luke Barnard 70ef8760cc Remove spurious console.dir 2018-07-04 11:56:02 +01:00
Luke Barnard c74d2d831b Add decryption error codes to base.DecryptionError
These should roughly follow https://github.com/matrix-org/matrix-ios-sdk/blob/9732cf593206a10d2b60cd01151a759c88c1e9a2/MatrixSDK/Crypto/Algorithms/MXDecryptionResult.h#L21-L39
2018-07-04 11:54:06 +01:00
David Baker 0415f821eb v0.10.5 2018-06-29 11:32:23 +01:00
David Baker 779fe35255 Prepare changelog for v0.10.5 2018-06-29 11:32:22 +01:00
Will Hunt fde00b1c62 getMediaLimits -> getMediaConfig 2018-06-23 12:57:29 +01:00
David Baker 11382d2cd7 v0.10.5-rc.1 2018-06-21 10:01:05 +01:00
David Baker ef31131a5d Prepare changelog for v0.10.5-rc.1 2018-06-21 10:01:05 +01:00
Will Hunt 8dd425f8ff Media/limits => /config 2018-06-20 17:24:45 +01:00
David Baker 76feabe32b Merge pull request #659 from matrix-org/t3chguy/media_fixes
fix auth header and filename=undefined
2018-06-18 13:46:59 +01:00
Michael Telatynski 7fe3e2f90a invert argument to make it positive without breaking backwards compat
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-06-18 12:27:25 +01:00
Michael Telatynski c0b2151929 allow omitting filename from upload entirely
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-06-15 18:25:20 +01:00
Michael Telatynski 5e3b1bf6b0 use Authorization header in media/v1/upload if enabled, instead of query
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-06-15 09:57:30 +01:00
Michael Telatynski fdf4523c2a export the Group model class (#656)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-06-14 13:52:13 +01:00
David Baker c6cf76f345 Merge branch 'master' into develop 2018-06-12 14:13:15 +01:00
David Baker f16e544691 v0.10.4 2018-06-12 14:11:07 +01:00
David Baker df101217fc Prepare changelog for v0.10.4 2018-06-12 14:11:07 +01:00
David Baker 5d90dc16cc Merge pull request #650 from matrix-org/t3chguy/audio_output
allow setting the output device for webrtc calls
2018-06-08 15:57:25 +01:00
David Baker bbfc1a1cd6 Merge pull request #596 from t3chguy/t3chguy/fix-docs
arguments true and false are actually invalid
2018-06-08 15:21:23 +01:00
Michael Telatynski b7208c12ac Merge pull request #643 from matrix-org/t3chguy/content-type-bugfix
fix typo where `headers` was not being used and thus sent wrong content-type
2018-06-08 12:29:35 +01:00
Michael Telatynski eaa2fdec44 Merge pull request #642 from matrix-org/t3chguy/doc_fixes
fix some documentation typos
2018-06-08 12:17:48 +01:00
David Baker 7099adfe79 v0.10.4-rc.1 2018-06-06 15:40:02 +01:00
David Baker cb17a2bcb0 Prepare changelog for v0.10.4-rc.1 2018-06-06 15:40:01 +01:00
Michael Telatynski 172044a1cb check whether notif level is undefined, because 0 is falsey (#651)
* check whether notif level is undefined, because `0` is falsey and it failed

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* improve number check for all m.room.power_levels related stuffs

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-05-29 12:11:29 +01:00
Michael Telatynski 4b203b6b63 allow setting the output device for webrtc calls
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-05-26 16:44:27 +01:00
David Baker 67876bab4c v0.10.3 2018-05-25 15:09:24 +01:00
David Baker 6ce691e40f Prepare changelog for v0.10.3 2018-05-25 15:09:23 +01:00
David Baker 9cd44b09f9 v0.10.3-rc.1 2018-05-24 18:16:59 +01:00
David Baker 73a2704126 Prepare changelog for v0.10.3-rc.1 2018-05-24 18:16:58 +01:00
Luke Barnard a50dd785b8 (Breaking Change) Add erase option to deactivateAccount (#649)
For erasing messages etc. after deactivation.

**Breaking change: `deactivateAccount` no longer takes callback**

Also: Move /account/deactivate from PREFIX_UNSTABLE to _R0
2018-05-24 10:47:41 +01:00
Luke Barnard bafbe5cbec Emit no_consent when M_CONSENT_NOT_GIVEN received (#647) 2018-05-22 18:02:21 +01:00
Will Hunt 9596087959 Remove extra return 2018-05-03 17:57:57 +01:00
Will Hunt 6570402b95 Add getMediaLimits to client 2018-05-03 13:43:37 +01:00
David Baker 4153845346 v0.10.2 2018-04-30 13:29:26 +01:00
David Baker 548713ed98 Prepare changelog for v0.10.2 2018-04-30 13:29:25 +01:00
Michael Telatynski 1bf1ce7070 fix typo where headers was not being used and thus sent wrong mime
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-04-30 06:56:43 +01:00
David Baker 21dc0fbf2f v0.10.2-rc.1 2018-04-25 14:57:08 +01:00
David Baker 7b29de9698 Prepare changelog for v0.10.2-rc.1 2018-04-25 14:57:08 +01:00
Michael Telatynski 7d468ee148 Replies (#607) (and Content Helpers)
* if event has `m.relates_to` extract this into the e2e-wrapper event

* Split out helpers to make content for {HTML,text}{emote,notice,message}
So that additional content fields can be added before sending

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-04-23 14:26:40 +01:00
Michael Telatynski 260e7b529f fix some documentation typos
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-04-15 23:31:33 +01:00
Michael Telatynski b027089fc1 use constants 2018-04-15 22:16:25 +01:00
David Baker 8f318528f8 Merge branch 'master' into develop 2018-04-12 12:17:45 +01:00
David Baker 9696e70817 v0.10.1 2018-04-12 12:06:25 +01:00
David Baker 85580acbff Prepare changelog for v0.10.1 2018-04-12 12:06:24 +01:00
David Baker c45a5f7e84 Name param slightly more helpfully 2018-04-12 12:00:41 +01:00
David Baker df74b0b038 Ignore inserts of dup inbound group sessions, pt 2
Stop the error from propagating to the txn error handler and
aborting the rest of the transaction.
2018-04-12 12:00:32 +01:00
David Baker 749e5b8458 Use the name of the error, not the request 2018-04-12 12:00:23 +01:00
David Baker 1255566324 Error message formatting 2018-04-12 12:00:11 +01:00
David Baker a25da4ebf4 Ignore inserts of duplicate inbound group sessions
Rather than aborting the whole txn

This is causing e2e migration fails in the wild
2018-04-12 11:59:59 +01:00
David Baker 72835f3c5f lint 2018-04-12 11:59:46 +01:00
David Baker c4f52f6256 Fix error logging 2018-04-12 11:59:32 +01:00
David Baker 3eb9505633 Log IDB errors 2018-04-12 11:58:33 +01:00
David Baker 543a0be1e4 Merge pull request #641 from matrix-org/dbkr/ignore_duplicate_inbound_group_session_2
Ignore inserts of dup inbound group sessions, pt 2
2018-04-12 11:25:54 +01:00
David Baker 5ef21c5075 Name param slightly more helpfully 2018-04-12 11:20:45 +01:00
David Baker 5c412cc0bd Ignore inserts of dup inbound group sessions, pt 2
Stop the error from propagating to the txn error handler and
aborting the rest of the transaction.
2018-04-12 11:15:39 +01:00
David Baker 346deb4b0d Use the name of the error, not the request 2018-04-11 18:57:52 +01:00
David Baker 6f8f30b018 Error message formatting 2018-04-11 18:56:17 +01:00
David Baker b808757012 Merge pull request #639 from matrix-org/dbkr/ignore_duplicate_inbound_group_session
Ignore inserts of duplicate inbound group sessions
2018-04-11 18:31:43 +01:00
David Baker 5e491bb89a Ignore inserts of duplicate inbound group sessions
Rather than aborting the whole txn

This is causing e2e migration fails in the wild
2018-04-11 17:27:25 +01:00
David Baker fc3cb0dd45 lint 2018-04-11 16:09:28 +01:00
David Baker 0846cb04af Fix error logging 2018-04-11 16:02:56 +01:00
David Baker 9708394846 Merge pull request #638 from matrix-org/dbkr/idb_log_error
Log IDB errors
2018-04-11 15:42:14 +01:00
David Baker 92e4396602 Log IDB errors 2018-04-11 15:37:28 +01:00
Luke Barnard a5870b73a9 Merge branch 'master' into develop 2018-04-11 11:39:26 +01:00
Luke Barnard 77ff6b9088 v0.10.0 2018-04-11 11:38:08 +01:00
Luke Barnard 425e74f297 Prepare changelog for v0.10.0 2018-04-11 11:38:08 +01:00
Luke Barnard ffa184464e v0.10.0-rc.2 2018-04-09 15:29:09 +01:00
Luke Barnard eca68e9e7f Prepare changelog for v0.10.0-rc.2 2018-04-09 15:29:09 +01:00
David Baker dd1bc8ec9a Update to new join policy API (#636) 2018-04-09 14:58:13 +01:00
Luke Barnard 24dc1d6991 Add wrapped to set group is_joinable (#634)
which is exposed in the summary API under `summary.profile.is_joinable`.

If true, anyone can join the group. If false, an invite is required.
2018-04-09 14:58:06 +01:00
Luke Barnard 9f510b7eee Add wrapper for group join API (#633)
Needed for new "joinable" groups.
2018-04-09 14:57:57 +01:00
David Baker 730ca9b60b Update to new join policy API (#636) 2018-04-05 17:02:43 +01:00
Luke Barnard 1afaf903f9 Add wrapped to set group is_joinable (#634)
which is exposed in the summary API under `summary.profile.is_joinable`.

If true, anyone can join the group. If false, an invite is required.
2018-03-29 17:03:20 +01:00
Luke Barnard d53cd41aa6 Add wrapper for group join API (#633)
Needed for new "joinable" groups.
2018-03-29 12:42:16 +01:00
David Baker 3e5ea745d2 Merge pull request #632 from matrix-org/dbkr/remove_ignore_call_log
Remove not very useful but veryv spammy log line
2018-03-27 18:28:08 +01:00
David Baker c7052f7dc7 Remove not very useful but veryv spammy log line 2018-03-27 18:21:03 +01:00
Richard Lewis 56b01df85b Merge pull request #628 from matrix-org/rxl881/stickerEvent
Switch event type to m.sticker.
2018-03-22 10:16:40 +00:00
David Baker aa18eeb7d6 v0.10.0-rc.1 2018-03-19 12:08:11 +00:00
David Baker c18264c615 Prepare changelog for v0.10.0-rc.1 2018-03-19 12:08:11 +00:00
David Baker 11746290a9 Merge pull request #630 from matrix-org/dbkr/fix_repeat_state_events_on_unpeek
Fix duplicated state events in timeline from peek
2018-03-16 15:44:38 +00:00
David Baker 16c062c069 Start first incremental sync request early (#629)
* Start first incremental sync request early

So it can run while we process our sync data.
2018-03-16 15:22:06 +00:00
David Baker 64396de0dc Fix duplicated state events in timeline from peek
When joining a room we were peeking into, we duplicated all the
state events into the timeline. Put back the old behaviour of just
setting them as state events, with copious commentary on how wrong
this seems.
2018-03-15 17:35:18 +00:00
Richard Lewis 8ff78c5d60 Switch event type to m.sticker. 2018-03-12 13:56:50 +00:00
David Baker 349297e495 Merge pull request #627 from matrix-org/dbkr/indexeddb_worker_on_start
Create indexeddb worker when starting the store
2018-03-09 17:43:15 +00:00
David Baker dc3ffb3b30 Fix jsdoc 2018-03-09 17:39:02 +00:00
David Baker 5c2dfb138a Hopefully clarify _startPromise 2018-03-09 17:35:03 +00:00
David Baker 0be679de42 Make local variable look less like a global 2018-03-09 17:31:08 +00:00
David Baker 8bd68e0f10 Create indexeddb worker when starting the store
Rather than when creating it, otherwise we could potentially end
up starting workers unnecessarily.
2018-03-09 16:53:27 +00:00
David Baker df70e16b4d Merge pull request #626 from matrix-org/dbkr/fix_indexeddb_logging
Fix indexeddb logging
2018-03-09 10:29:53 +00:00
David Baker b246545da5 Merge remote-tracking branch 'origin/develop' into dbkr/fix_indexeddb_logging 2018-03-09 10:21:00 +00:00
David Baker 3280cb648f Merge pull request #625 from matrix-org/dbkr/stop_spinner_of_doom
Don't do /keys/changes on incremental sync
2018-03-09 10:19:26 +00:00
David Baker 8798bf42e6 Fix indexeddb logging
1. Fix double 'loaded' on sync data logging
2. Move the 'loaded' message into the bit where the data has
   actually loaded rather than the promise try block.
3. Add '...' to the 'loading' messages so they're easier to tell
   apart from the 'loaded' messages.
2018-03-09 10:16:32 +00:00
David Baker 5a23927e56 Move comment up 2018-03-09 10:09:36 +00:00
Matthew Hodgson beafd597dd ensure indexeddb workers are never double-connected 2018-03-09 02:18:19 +00:00
Matthew Hodgson fbc43b0d58 stupid typo 2018-03-09 00:01:14 +00:00
David Baker facfcf679d DeviceList: bring save forward if necessary
If save is called with a delay that would want the save to happen
sooner then the save we currently have scheduled, cancel the
current save and schedule a new one for the sooner time.
2018-03-08 15:35:35 +00:00
Luke Barnard 68b230a78f Add function to cancel and resend key request (#624) 2018-03-08 15:01:01 +00:00
David Baker 3d1fcc6f83 One day I'll learn to spell guaranteed 2018-03-08 14:26:48 +00:00
David Baker a0578efeb9 fix tests 2018-03-08 13:52:48 +00:00
David Baker 727ad5755e lint 2018-03-08 12:40:01 +00:00
David Baker 4f17352858 Don't do /keys/changes on incremental sync
Remove the call to /keys/changes when we do an incremental syn
where the old sync token doesn't match the one in the device list
store. To allow us to do this, always save the device list store
before saving the sync data, so we can safely assume the device
list store is at least as fresh as the sync token in the sync store.

Thread save functions through to allow this, add a delay parameter
so the sync can save the device list immediately and skip the wait,
and add a wantsSave() method so the sync can skip saving the device
list if the sync store isn't going to save anyway.

Fixes https://github.com/vector-im/riot-web/issues/6068
2018-03-08 12:33:08 +00:00
David Baker 0eb72122ce Merge pull request #623 from matrix-org/dbkr/devicelist_is_too_dirty
Don't mark devicelist dirty unnecessarily
2018-03-07 17:55:38 +00:00
David Baker 66e2b3bb70 Don't mark devicelist dirty unnecessarily
This was marking the device list as dirty even when nothing had
actually changed, causing unnecessary saves.
2018-03-07 17:32:51 +00:00
David Baker 5f12d858eb Keep a push processor and re-use it. (#622)
Because it does some nice caching stuff but that's no good if we
re-create a new one each time.
2018-03-06 19:01:30 +00:00
Luke Barnard e258d6ca8d Fix bug in crypto-store-backend; use oncomplete (#620)
instead of onsuccess for the txn to add an outgoing room key
request.
2018-03-02 12:01:21 +00:00
David Baker 2d25dedbcc Merge pull request #619 from matrix-org/dbkr/cache_joined_member_count
Cache the joined member count for a room state
2018-02-26 16:56:26 +00:00
David Baker bdf6fcb222 Fix tests 2018-02-26 16:53:17 +00:00
David Baker 7e1cea1ef6 Cache the joined member count for a room state
Pushrule evaluation needs the count of joined room members and
filtering the list for joined members takes a nontrivial amount
of time, but caching it is trivial, both code and memory wise.
2018-02-26 16:43:26 +00:00
Richard Lewis d98c803b54 Merge pull request #618 from matrix-org/rxl881/stickers
Fix JS doc
2018-02-23 15:52:42 +00:00
Matthew Hodgson 95238466b5 Merge pull request #617 from matrix-org/dbkr/fix_push_actions_frozen_matrixevents
Precompute push actions for state events
2018-02-23 14:32:11 +00:00
Matthew Hodgson 71652043a0 improve doc 2018-02-23 14:31:51 +00:00
David Baker c9cbaf254b Precompute push actions for state events
State events get froze now, so we can't write to the push actions
cache after having done so: precompute the push actions to work
around this.
2018-02-23 11:06:22 +00:00
David Baker f9cc5cbd33 Merge pull request #616 from matrix-org/luke/fix-unverified-device-blacklist
Fix bug where global "Never send to unverified..." is ignored
2018-02-23 10:11:59 +00:00
Luke Barnard bcb9405793 Instead of emitting, rely on the promise 2018-02-22 18:41:18 +00:00
Luke Barnard 30cb6f196f Fix overriding undefined per-room unverified devices setting
If the per-room setting for preventing sending keys to unverified
devices is `undefined`, it overrides the global setting (which
could be `true`).
2018-02-22 18:02:39 +00:00
Luke Barnard 856ef01632 Emit a crypto.initComplete once crypto is initialised
So that the app can call crypto-dependent functions at
the correct point in time.
2018-02-22 17:59:37 +00:00
David Baker 6f95554655 Merge pull request #615 from matrix-org/dbkr/intern_legacy_membership
Intern legacy top-level 'membership' field
2018-02-22 17:01:38 +00:00
David Baker a72f915646 Intern legacy top-level 'membership' field
There is a legacy top-level 'membership' field on events. We were
interning the normal one in 'content', but not this one, so the
legacy field was still keeping a copy of the string 'join' / 'leave'
etc in memory for every member event.

This make the interning code know about this field.
2018-02-22 16:04:35 +00:00
Matthew Hodgson 94605417f6 Merge pull request #598 from matrix-org/t3chguy/implicit_rr_redaction
Don't synthesize RR for m.room.redaction as causes the RR to go missing.
2018-02-20 18:23:06 +00:00
David Baker 1e017df128 Merge pull request #613 from matrix-org/dbkr/dates_on_demand
Make Events create Dates on demand
2018-02-20 18:08:45 +00:00
David Baker ec27bb5131 Merge pull request #612 from matrix-org/dbkr/dont_clone_events
Stop cloning events when adding to state
2018-02-20 18:08:35 +00:00
David Baker 9637fc098a comment 2018-02-20 18:01:01 +00:00
David Baker 874020ced7 Make Events create Dates on demand
My test account had 37MB (shallow) of Date objects knocking around
in memory. This gets rid of them. I can't see any appreciable
difference in the time taken to switch rooms (where now we recreate
a bunch of Dates that previously would have been cached).
2018-02-20 17:49:25 +00:00
David Baker a7beedcfb6 Unused imports 2018-02-20 17:11:40 +00:00
David Baker 4351c4dd6f Merge pull request #611 from matrix-org/dbkr/use_initialisestate
De-dup code: use the initialiseState function
2018-02-20 17:10:11 +00:00
David Baker 1ddf7fb96c Add XXX comment 2018-02-20 17:07:14 +00:00
David Baker ec5cfe4ee9 Stop cloning events when adding to state
As comment hopefully explains.

On my test account:
Before: 394657 MatrixEvents, 53MB shallow size
After: 198863 MatrixEvents, 27MB shallow size
2018-02-20 16:45:20 +00:00
Matthew Hodgson 4fed2ea7bf log event IDs of ignored calls 2018-02-20 12:38:12 +00:00
Matthew Hodgson ae14cf4740 typo 2018-02-20 12:38:12 +00:00
David Baker 8aa68b3dc1 lint 2018-02-20 11:09:32 +00:00
David Baker e810ee7750 Add test for fixed state misbehaviour 2018-02-20 11:06:33 +00:00
David Baker 9a08194597 Factor out calls to getLiveTimeline 2018-02-19 16:17:30 +00:00
David Baker c77277b60c rename variable 2018-02-19 16:07:28 +00:00
David Baker 21a324558f Comments 2018-02-19 16:05:43 +00:00
David Baker 7a31751564 Merge pull request #610 from matrix-org/dbkr/sentinels_on_demand
Create sentinel members on-demand
2018-02-16 19:01:48 +00:00
David Baker b11bacc2e2 Process state events before timeline events
Don't ignore them: its valid to send them in a non-limited sync,
they're state events that preceed the timeline events.
2018-02-16 18:52:56 +00:00
David Baker 8c02e7ba67 comment 2018-02-16 17:52:39 +00:00
David Baker 275eb8d434 De-dup code: use the initialiseState function
This should behave identically, but the code here appeared to be
identical to the code in initialiseState, so let's use it (it also
has an extra sanity check in there that we only init empty timelines).
2018-02-16 17:49:29 +00:00
David Baker 736d0df38d Handle null userID
The tests inject messages with no sender, so for now let's maintain
behaviour as it was before.
2018-02-16 14:58:19 +00:00
David Baker 89d5d41015 Merge pull request #609 from matrix-org/dbkr/doc_sentinels
Some more doc on how sentinels work
2018-02-16 12:34:34 +00:00
David Baker 0e1444c84b grammar 2018-02-16 11:57:22 +00:00
David Baker 104f8b093d Remove redundant forEach 2018-02-16 11:55:45 +00:00
David Baker 1e638c376b Create sentinel members on-demand
We only need sentinel members for things like the 'sender' field
of events, so we previously created sentinels for everyone in the
room, but a large number of them were never used.

Instead, create them on-demand and cache them.
2018-02-16 11:53:24 +00:00
David Baker a2e1a6ca8f jsdoc doesn't like that 2018-02-16 10:59:44 +00:00
David Baker 337331ff1b Some more doc on how sentinels work 2018-02-16 10:37:50 +00:00
Richard Lewis 1dfde7cd80 Fix JS doc 2018-02-08 11:17:55 +00:00
Luke Barnard 7df2bfe7bc Honour cached push rules (#606)
so that we have push rules loaded into the client after doing
a cached sync (so that the client can assume that we do have
push rules, even if it was loaded from a cached sync).

These rules will be updated once `getPushRules` is completed
prior to the first networked sync.
2018-02-08 09:35:44 +00:00
lukebarnard 4ec90a4b99 Fix incorrect self - should be this 2018-02-07 17:31:16 +00:00
Luke Barnard d4e8f9039c Set sync token before incrementally syncing (#604)
Block on syncing from cache so that the sync token
has been set prior to incremental syncing.
2018-02-07 17:16:22 +00:00
Luke Barnard 4c6c00f16d Fix NPE when loading sync from cache (#603)
* Fix NPE when loading sync from cache
2018-02-07 16:04:47 +00:00
Luke Barnard fd30b25596 Allow a mode of operation without HS connection (#601)
* Allow a mode of operation without HS connection

Instead of blocking the first sync on getting the push rules and
filter, load sync data from disc.

When the client comes online, the push rules will be acquired and
a sync cycle started. This could be immediate if the client is
already online.

This could be a breaking change for clients that get push rules
before a successful sync has been done.
2018-02-06 17:46:39 +00:00
David Baker 032b7cab5d Fix name of 1:1s other person has left (#602)
Use their name as we did before rather than 'Empty Room'

Fixes https://github.com/vector-im/riot-web/issues/6083
2018-02-06 15:43:53 +00:00
David Baker 47dfb4b8cd Merge pull request #597 from matrix-org/dbkr/e2e_rooms_indexeddb
Migrate room encryption store to crypto store
2018-02-06 10:29:29 +00:00
David Baker 51e782b671 Merge pull request #600 from matrix-org/t3chguy/fix_3pid_dm_rooms
add parameter to getIdentityServerUrl to strip the protocol for invites
2018-02-05 12:07:31 +00:00
David Baker cf195262bf inline fucntion that was only used once 2018-01-31 18:12:46 +00:00
David Baker cf72052e46 Correct return type 2018-01-31 17:51:48 +00:00
David Baker 6f50c39b2a more doc 2018-01-31 17:49:58 +00:00
David Baker b6cd826dd7 basically I just can't type 2018-01-31 17:33:35 +00:00
David Baker f1194b1fbe more comment grammar 2018-01-31 17:33:02 +00:00
David Baker c0ca85fb3a comment grammar 2018-01-31 17:32:12 +00:00
David Baker 022df1b143 Revert npm helpfully adding packages 2018-01-31 17:30:12 +00:00
David Baker fdf987f081 Merge remote-tracking branch 'origin/develop' into dbkr/e2e_rooms_indexeddb 2018-01-29 13:40:14 +01:00
David Baker f1e874cd18 Merge pull request #594 from matrix-org/dbkr/device_tracking_indexeddb
Move Device Tracking Data to Crypto Store
2018-01-29 13:38:50 +01:00
David Baker c3bede58aa Fux lying comment 2018-01-29 13:35:03 +01:00
Matthew Hodgson 38915eb7fc fix NPE biting yannick due to broken rules 2018-01-25 18:41:36 +01:00
David Baker 470bd23b3b Add new devices as we get them 2018-01-25 16:49:30 +01:00
Michael Telatynski d007eefe2e add parameter to getIdentityServerUrl to strip the protocol for invites
use new getIdentityServerUrl param in inviteByThreePid
2018-01-25 09:53:19 +00:00
Michael Telatynski 362f442a98 Don't synthesize RR for m.room.redaction as causes the RR to go missing. 2018-01-25 00:04:24 +00:00
David Baker 8c2645c5dd Comment typo 2018-01-24 21:39:46 +01:00
David Baker 1ba0e4809e Implement correct function
in localstorage and memory stores
2018-01-22 18:46:13 +01:00
David Baker 341371b613 lint 2018-01-22 18:37:02 +01:00
David Baker d856285271 Doc param 2018-01-22 18:35:29 +01:00
David Baker e4ffc93463 Remove unused function 2018-01-22 18:34:34 +01:00
David Baker 3149958319 Missed arg 2018-01-22 18:31:17 +01:00
David Baker ac659e8df1 remove debugging 2018-01-22 18:16:11 +01:00
David Baker 81d54c7558 comment typo 2018-01-22 18:11:37 +01:00
David Baker e4de333d83 Use the right prefix function to remove prefix 2018-01-22 17:55:40 +01:00
David Baker e72096328a Actually working migration 2018-01-22 17:46:13 +01:00
David Baker 88a082a533 Hopefully working migration 2018-01-22 17:34:09 +01:00
David Baker 4fbf4f1069 Hopefully address PR feedback 2018-01-22 17:08:19 +01:00
David Baker c360dd11ed Migrate e2e rooms to crypto store
Doesn't do data migration yet
2018-01-22 15:48:53 +01:00
Michael Telatynski 95d582ccee arguments true and false are actually invalid 2018-01-19 23:00:34 +00:00
David Baker 074cfb7c58 Add more comments 2018-01-18 17:32:47 +00:00
David Baker 8b649cec8d All the copyrights 2018-01-18 11:52:27 +00:00
David Baker ea6974fc89 Return null if device data isn't set
This is more normal, and the code doesn't expect to get empty
objects here which is reasonable since it never sets one.
2018-01-17 19:27:22 +00:00
David Baker fb29da4e40 Update the catchingup flag
It may change whilst processing the sync
2018-01-17 19:17:48 +00:00
David Baker 461acbcc81 More test fixing
* Change test for new storage layer
 * Always store device keys we download, even if we weren't
   tracking the user.
2018-01-17 18:49:33 +00:00
David Baker 5bab8647b6 Fix device tracking with initial /sync
* Check whether we share an e2e room with user IDs in the 'left'
   field of /keys/changes: there's no guarantee we no longer share
   any e2e rooms with these users
 * Reset everyone's tracking status on an initial sync - just
   re-fetching device lists for all users we're currently tracking
   isn't good enough since room memberships may have changed.
 * Fix typo in test
2018-01-17 16:03:46 +00:00
David Baker f2d1222de7 Merge pull request #591 from matrix-org/luke/perf-push-processor
Optimise pushprocessor
2018-01-17 11:27:50 +00:00
David Baker 585ea14a23 more lint 2018-01-17 09:58:56 +00:00
David Baker 5a0997ded5 lint 2018-01-17 09:52:01 +00:00
David Baker 0174c5674f Make tests pass, finally.
Mostly making tests aware of new storage format or making them
force it to be written. Also some bugfixes like we didn't json
encode some things in the localstorage store and we didn't
correctly check the promise when requesting device data saves.
2018-01-16 17:57:49 +00:00
lukebarnard 9de8653936 Actually check the cache 2018-01-16 09:51:30 +00:00
David Baker 27d28b8247 More test fixing
Allow localstorage store to take a localstorage impl, make TestClient
pass a cryptostore & fix True/true typo
2018-01-15 16:27:28 +00:00
David Baker 110f43a246 Make DeviceList test pass
Includes making saveIfDirty() return a promise in case you care
about when changes got saved (which the test does).
2018-01-15 15:21:39 +00:00
lukebarnard 56612751f9 Handle underride and override rules with patterns 2018-01-15 15:16:22 +00:00
lukebarnard 751fe7349a Fix case insensitivity on new RegExps 2018-01-15 14:54:51 +00:00
David Baker 36d7d33afc Null fix & lint
Don't end up with devices / device tracking status being null
2018-01-12 16:44:51 +00:00
David Baker a94f3c720e Fix migration 2018-01-12 15:12:57 +00:00
David Baker 6c1087e429 Migrate from session store 2018-01-12 14:41:08 +00:00
David Baker 2cdb010cff Fix lint 2018-01-12 14:17:10 +00:00
David Baker 60052f59a0 Add localstorage & memory device data stores 2018-01-12 14:05:34 +00:00
David Baker 29d44f809e delay saving so we can batch multiple operations 2018-01-12 12:01:00 +00:00
David Baker 83e4aa2755 Initial attempt at device tracking -> indexeddb
* Message sending works again, but

 * Marking multiple devices known (ie. 'send anyway') is very slow
   because it writes all device info out each time
 * Support for non-indexedb stores not written yet
 * No migration
2018-01-11 18:10:19 +00:00
David Baker eeb97f5b66 Merge pull request #592 from matrix-org/dbkr/set_event_error
Set event error before emitting
2018-01-09 18:08:12 +00:00
David Baker b6f26ae6a5 Oops: new year, new company 2018-01-09 18:05:02 +00:00
Richard Lewis f40435654a Merge pull request #590 from matrix-org/rxl881/stickers
Add event type for stickers [WIP]
2018-01-09 14:49:05 +00:00
David Baker 67d471ea3d Also happy new year and all that 2018-01-09 13:24:29 +00:00
David Baker 4946c5e687 Set event error before emitting
So the event object properties are in a consistent state when we
the event is emitted.

Fixes first part of https://github.com/vector-im/riot-web/issues/5936
2018-01-09 13:23:04 +00:00
David Baker af4f05c29e Merge pull request #587 from matrix-org/dbkr/inbound_sessions_to_cryptostore
Migrate inbound sessions to cryptostore
2018-01-09 10:41:38 +00:00
David Baker 145c76095a Remove unused countEndToEndInboundGroupSessions 2018-01-09 10:37:44 +00:00
Richard Lewis 90045b6faa Fix filtering. 2018-01-08 11:18:36 +00:00
Richard Lewis aef27d811a Set sticker type on event instead of message type. 2018-01-06 00:13:24 +00:00
lukebarnard e2e5f80298 Linting 2018-01-05 20:05:12 +00:00
lukebarnard ca0ed50172 Optimise pushprocessor
by not [re]creating RegExps unnecessarily.
2018-01-05 19:56:27 +00:00
Richard Lewis d44d63c1d6 Send sticker message in to a room. 2018-01-04 21:55:20 +00:00
David Baker 8403042297 Merge pull request #588 from pafcu/displaynames
Disambiguate names if they contain an mxid
2018-01-02 17:46:04 +00:00
David Baker 1568bb014d Merge remote-tracking branch 'origin/develop' into dbkr/inbound_sessions_to_cryptostore 2018-01-02 17:12:47 +00:00
David Baker 7a069c4018 doc return 2018-01-02 17:03:57 +00:00
David Baker c856eb931f Make error message more truthful 2018-01-02 16:32:33 +00:00
David Baker b290ce795f Update comment 2018-01-02 16:21:12 +00:00
David Baker 6f3d279165 Fix doc 2018-01-02 13:43:43 +00:00
David Baker 124ab30f98 jsdoc 2018-01-02 13:42:16 +00:00
David Baker fee90bab66 Wording fix 2018-01-02 13:35:52 +00:00
David Baker e26ade0e62 Wording fix 2018-01-02 13:32:51 +00:00
David Baker c43ccb860b Always migrate inbound group sessions 2018-01-02 13:31:57 +00:00
Richard van der Hoff f2b7e8b038 Remove spurious linty blank line 2017-12-14 22:36:53 +00:00
Stefan Parviainen 1863f1311b Disambiguate display name if it contains a mxid
Fixes https://github.com/vector-im/riot-web/issues/1811

Signed-off-by: Stefan Parviainen <pafcu@iki.fi>
2017-12-14 21:49:13 +01:00
David Baker e5086f22d6 Merge remote-tracking branch 'origin/develop' into dbkr/inbound_sessions_to_cryptostore 2017-12-08 19:06:36 +00:00
David Baker da90a3ca78 Merge pull request #585 from matrix-org/dbkr/count_sessions_before_migrate
Check for sessions in indexeddb before migrating
2017-12-08 18:29:50 +00:00
David Baker d397c5a251 Merge pull request #586 from matrix-org/dbkr/crypto_store_migrate_warning
Emit an event for crypto store migration
2017-12-08 18:17:23 +00:00
David Baker d26b4434b8 Merge pull request #575 from matrix-org/dbkr/udd_no_auto_show
Supporting fixes For making UnknownDeviceDialog not pop up automatically
2017-12-08 16:33:18 +00:00
David Baker 7188f17f9a more linting 2017-12-07 17:14:11 +00:00
David Baker b0365f8b0e json encode before saving to localstorage 2017-12-07 17:07:06 +00:00
David Baker a1ddeea00e Fix inbound group session migration
Apparently they are parsed at a different layer
2017-12-07 16:50:18 +00:00
David Baker 1414c24caf Missed txn 2017-12-07 15:03:02 +00:00
David Baker 0748c864cd Migrate inbound sessions from session store 2017-12-07 14:57:18 +00:00
David Baker f80626a0fa lint 2017-12-07 14:32:23 +00:00
David Baker 0fffd64a7f Fix key export 2017-12-07 14:23:36 +00:00
David Baker 8da48211d9 Fix exportInboundGroupSession 2017-12-06 22:42:19 +00:00
David Baker 0dd8ffa3a0 Much tedious linting 2017-12-06 22:36:24 +00:00
David Baker 0362e61f36 Unused var 2017-12-06 22:24:57 +00:00
David Baker d92a77f695 lint 2017-12-06 22:24:19 +00:00
David Baker e6dd573e8a Fix test (needs a cryptostore now) 2017-12-06 19:42:01 +00:00
David Baker b6330c3a4f er, this isn't an object 2017-12-06 19:41:44 +00:00
David Baker 10bc714f5c lint 2017-12-06 16:04:00 +00:00
David Baker 8f57723b88 Emit an event for crypto store migration
As we may want to warn the user to not go back to an older version.
2017-12-06 15:24:29 +00:00
David Baker 3d71bee85e Check for sessions in indexeddb before migrating
Don't migrate sessions from localstorage if we already have some
in indexeddb
2017-12-06 12:43:08 +00:00
David Baker 30e00d5fa7 Add impl to localstorage & memory store 2017-12-06 12:02:05 +00:00
David Baker bc99a9d792 Merge remote-tracking branch 'origin/develop' into dbkr/inbound_sessions_to_cryptostore 2017-12-06 10:05:33 +00:00
David Baker 61df41d21f Merge pull request #584 from matrix-org/dbkr/sessions_to_cryptostore
Move sessions to the crypto store
2017-12-06 10:04:33 +00:00
David Baker 0b4ef8dcbb Migrate inbound group sessions to crypto store 2017-12-05 21:47:22 +00:00
David Baker 9d7d48b9b5 s/sessionStore/cryptoStore/ 2017-12-05 14:04:18 +00:00
David Baker fb37150dfd Doc exception handling fun 2017-12-05 13:45:37 +00:00
David Baker bd08ed898d Maybe slightly better exception handling 2017-12-05 11:11:14 +00:00
David Baker d3bc525713 More doc 2017-12-05 10:23:26 +00:00
David Baker ae27c553ac More sensible loop & logging 2017-12-05 10:13:34 +00:00
David Baker 27030ae1e9 Use _unpickleSession in _getSession 2017-12-05 10:09:54 +00:00
David Baker fd083e1e66 doc 2017-12-05 10:04:06 +00:00
David Baker 5e4149ae76 Remove unused function 2017-12-04 18:14:56 +00:00
David Baker 859d462629 Lint 2017-12-04 18:08:38 +00:00
David Baker 8bfa81df42 Catch & rethrow decryption exceptions 2017-12-04 17:55:12 +00:00
David Baker 6782d53e28 Remove prefix from sessions store keys 2017-12-04 17:36:33 +00:00
David Baker 4796721d5c Migrate sessions from sessionStore 2017-12-04 17:12:13 +00:00
David Baker 55bbc71a17 Move sessionstore -> cryptostore migration
...into a separate function
2017-12-04 16:08:52 +00:00
David Baker 5372575b24 Implement session storage in memory & localstorage 2017-12-04 15:49:59 +00:00
David Baker d995019c6e lint 2017-12-04 14:40:32 +00:00
David Baker aa70da5659 Move sessions to the crypto store
This doesn't migrate existing ones yet
2017-12-04 14:33:03 +00:00
Luke Barnard 020b293068 Merge branch 'master' into develop 2017-12-04 11:58:30 +00:00
Luke Barnard 678ff23bcb v0.9.2 2017-12-04 11:57:05 +00:00
Luke Barnard f93d50dcd0 Prepare changelog for v0.9.2 2017-12-04 11:57:04 +00:00
Luke Barnard aa3201ebb0 Send correct m.visibility flag in js-sdk 2017-12-04 11:54:42 +00:00
Luke Barnard 4ad153c425 Send correct m.visibility flag in js-sdk 2017-12-01 18:01:23 +00:00
David Baker 082683bf0e Merge pull request #582 from matrix-org/dbkr/crypto_store_txn_api
Change crypto store transaction API
2017-11-30 13:45:28 +00:00
Luke Barnard 7f590af0b5 Add API wrapper for multiple device deletion API (#583) 2017-11-30 10:16:18 +00:00
David Baker ecc1c86600 Doc some more things 2017-11-29 17:40:48 +00:00
David Baker fece506cdd delint 2017-11-29 16:35:12 +00:00
David Baker f11a58e2cb Change crypto store transaction API
To allow multiple things to be fetched/stored in a single
transaction.

Currently it is still just the account that's actually in
indexeddb though.
2017-11-29 16:22:54 +00:00
David Baker 0238ecebed Fix comment 2017-11-28 11:32:34 +00:00
Richard van der Hoff 0d6ffa3935 Merge pull request #581 from matrix-org/dbkr/copyrights
Add some missed copyright notices
2017-11-27 16:04:07 +00:00
David Baker 143632e635 Add some missed copyright notices 2017-11-27 15:56:35 +00:00
David Baker 9ec33a97bb Merge pull request #579 from matrix-org/dbkr/e2e_indexeddb
Move Olm account to IndexedDB
2017-11-27 13:57:27 +00:00
David Baker 7e2c236582 Missed a s/account data/picked account/ 2017-11-27 13:46:31 +00:00
David Baker 6ebfd175bc jsdoc clarifications 2017-11-22 18:37:16 +00:00
David Baker defaa918a6 Remove unused function 2017-11-22 18:26:26 +00:00
David Baker c4e70be0a5 Better comment wording 2017-11-22 18:25:25 +00:00
David Baker 57d425fae6 Make the save function not return a promise
This was entirely unnecessary and hopefully make things a bit
simpler to understand and has fewer asyncs flying around.
2017-11-22 18:05:08 +00:00
David Baker 6024163af8 s/accountData/pickledAccount/ 2017-11-22 17:50:00 +00:00
David Baker 44b35cdb3d Lint 2017-11-22 16:53:21 +00:00
David Baker 36ff0ad019 Merge pull request #580 from matrix-org/rav/fix_crypto_error_logging
Fix logging of DecryptionErrors to be more useful
2017-11-22 16:44:09 +00:00
David Baker 9218e518f1 Add LocalStorageCryptoStore
To avoid throwing away all the data for anyone running firefox in
one of the modes where indexedDB is broken.
2017-11-22 16:41:52 +00:00
Richard van der Hoff c80bde1f60 Fix logging of DecryptionErrors to be more useful
We were relying on being able to override toString in DecryptionError, which
(a) doesn't work thanks to https://github.com/babel/babel/issues/3083, and (b)
was a bit naughty anyway. Instead, just add a detailedString property and use
that.
2017-11-22 14:42:32 +00:00
David Baker 4b7157b987 Remove unnecessary 'if' 2017-11-22 14:31:06 +00:00
David Baker a90f592224 Add comment on deprecation 2017-11-22 14:12:35 +00:00
David Baker 59f228dab7 Migrate account from session store 2017-11-22 14:07:19 +00:00
David Baker bae3f5ceb7 It's a heap, not a stack 2017-11-22 10:19:27 +00:00
David Baker a5c5da5b8a Lint 2017-11-22 10:18:53 +00:00
David Baker 7ecf313132 Use a callback function at the store layer
Rather than a promise which relies on the caller's promise handler
code being run in the same tick which is not guaranteed.
2017-11-22 10:04:27 +00:00
David Baker 313cfacfa1 Add comment 2017-11-21 18:40:25 +00:00
David Baker fb991503a9 Move OLM account to IndexedDBd
Wraps all access to the account in a transaction so any updates
done to the account must be done in the same transaction, making the
update atomic between tabs.

Doesn't do any migration from localstorage yet.
2017-11-21 18:27:40 +00:00
David Baker c31ce641a1 Merge branch 'master' into develop 2017-11-17 15:57:22 +00:00
David Baker 5b1a5b7dd0 v0.9.1 2017-11-17 15:55:21 +00:00
David Baker f25324fb1c Prepare changelog for v0.9.1 2017-11-17 15:55:20 +00:00
David Baker 2a7f35a633 Merge pull request #577 from matrix-org/dbkr/fix_force_turn
Fix the force TURN option
2017-11-17 15:51:01 +00:00
David Baker eb2d5484b8 jsdoc and remove pointless return 2017-11-17 15:31:50 +00:00
David Baker 40cbd5ec9d Honour old forceTURN option for now 2017-11-17 15:03:12 +00:00
David Baker 10680ace17 Fix the force TURN option
Option needed to be passed in when creating a webrtc call, but for
incoming calls the js-sdk creates the call itself, so the app never
gets a chance to set the option.
2017-11-17 14:51:37 +00:00
David Baker 26e28ed687 Don't spuriously send unknown devices error
Send a sensible error message for other errors.
2017-11-17 11:48:56 +00:00
David Baker 8bf92d84db oops - didn't mean to remove that bit of doc 2017-11-16 18:12:09 +00:00
David Baker 9e2bb5b37b Allow answer to be called again after failing
* Store the answer we generate so if we fail to send it, we can
   try to send it again (doing the same again doesn't work as
   webrtc is in the wrong state).
 * Don't send ICE candidates if the call is ringing: queue them up
   so we can send them later if we manage to actually send the
   answer.
2017-11-16 16:29:45 +00:00
David Baker a48a88c312 Don't send a hangup on user media failure
We won't have sent the invite anyway. Also termainate before we
fire the error event so the call is 'ended' when the event handlers
fire (which means if they try to hang up it's also ignored)
2017-11-15 17:18:47 +00:00
David Baker 76b2fc2a6c Merge remote-tracking branch 'origin/develop' into dbkr/udd_no_auto_show 2017-11-15 12:10:06 +00:00
Luke Barnard 4438d716b9 Merge branch 'master' into develop 2017-11-15 10:57:57 +00:00
David Baker 8fcf55d761 BREAKING CHANGE: Fixes for unknown device errors
* If we can't send an invite due to unknown devices, abort the
   call.
 * Don't transition to the `invite_sent` state until the invite
   has actually sent.
 * Add a specific error code for failure due to unknown devices.
 * Don't send ICE candidate messages if the call has ended.
 * Add an `event` property to errors from `sendEvent` so that the
   caller can resend or cancel the event.
2017-11-15 10:56:57 +00:00
Luke Barnard 3091a76702 v0.9.0 2017-11-15 10:56:31 +00:00
Luke Barnard 117e2370d7 Prepare changelog for v0.9.0 2017-11-15 10:56:31 +00:00
Matthew Hodgson a35d70e995 Merge pull request #568 from turt2live/travis/granular-settings
[BREAKING] Change the behaviour of the unverfied devices blacklist flag
2017-11-15 10:31:11 +00:00
Travis Ralston ec68000105 Merge branch 'develop' into travis/granular-settings 2017-11-14 19:19:01 -07:00
Matthew Hodgson c707d3db00 Merge pull request #557 from turt2live/travis/presence
Support set_presence=offline for syncing
2017-11-14 23:41:37 +00:00
Matthew Hodgson d3572836bd Merge pull request #556 from turt2live/travis/improved-redact-check
Consider cases where the sender may not redact their own event
2017-11-14 23:25:06 +00:00
David Baker a5dac751b0 BREAKING CHANGE: Remove send_event_error
Reverts https://github.com/matrix-org/matrix-js-sdk/pull/378

This swallowed all errors from sendEvent, breaking the ICE candidate
retrying. react-sdk no longer listens for send_event_error so I
think it's best to just remove this.
2017-11-13 17:51:59 +00:00
Travis Ralston 7a59579dcd Support sending additional options with acceptance (#570)
To be able to send things like `{visibility: { type: 'private ' }}`

Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-11-10 14:14:11 +00:00
Luke Barnard a60e4efe6d v0.9.0-rc.1 2017-11-10 13:30:24 +00:00
Luke Barnard 4c0ebeab58 Prepare changelog for v0.9.0-rc.1 2017-11-10 13:30:24 +00:00
David Baker 0143ac2a86 Ignore pushrules in our cached sync response (#571)
* Ignore pushrules in our cached sync response

As hopefully explained in the comment
2017-11-10 13:14:54 +00:00
Travis Ralston f24b02cae4 It helps if you use the right function
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-11-09 11:01:42 -07:00
Luke Barnard 497a2bd057 Update API wrapper for group room association configuration (#569)
This is now a generic API on the homeserver. This is wrapped in a non-generic js-sdk function for now. In future, other group server implementations may want specific configuration and the configuration keys are namespaced to prevent collisions there.
2017-11-09 17:34:26 +00:00
Travis Ralston 995f796a5d [BREAKING] Change the behaviour of the unverfied devices blacklist flag
Previously the global flag was used as a way to completely ignore the per-room option. This commit makes the per-room and global settings be more flexible to allow users to, for example, blacklist unverified devices in all room with the exception of one or two. This is done by making the global setting a device-level default and the per-room option allowing for 3 states: true, false, and unset (use device default).

Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-11-08 17:47:45 -07:00
Travis Ralston bf462e2840 Install babel-runtime, as it is required for operation (#561)
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-11-08 14:34:08 +00:00
Matthew Hodgson fb75134179 unbreak tests 2017-11-06 15:17:06 +00:00
Matthew Hodgson 68d67b7fc2 Remove ugly english non-i18n invite descriptions
any sensible app should be synthesising a better name for invites which is i18n-aware.
2017-11-06 11:18:24 +00:00
David Baker bbf412f3ad Merge pull request #567 from matrix-org/luke/groups-update-group-room-assoc
Modify addRoomToGroup to allow setting isPublic, create alias updateGroupRoomAssociation
2017-11-02 14:55:01 +00:00
David Baker 52b575296c Merge pull request #565 from matrix-org/dbkr/get_rule_by_id
Expose more functionality of pushprocessor
2017-11-02 13:49:00 +00:00
David Baker c1652f4898 Merge pull request #566 from matrix-org/dbkr/may_trigger_notif
Function for working out notif trigger permission
2017-11-02 13:48:07 +00:00
Luke Barnard d40cc795f7 Modify addRoomToGroup to allow setting isPublic, create alias updateGroupRoomAssociation
The API on synapse was modified to be an upsert, which means the same API call for adding a room to a group can be used to update the visibility of that association.
2017-11-02 13:34:34 +00:00
David Baker 4035d933ad Use the default power level if there's no PL event 2017-11-02 13:28:58 +00:00
David Baker d255348762 Remove leave check
1. It's wrong because it doesn't consider the ban state
2. This should be caught by whether they have permission to send
   the event in the first place rather than worrying about whether
   it has notification permission or not
2017-11-02 13:26:39 +00:00
David Baker 978db89deb Function for working out notif trigger permission
And make pushprocessor use this function
2017-11-02 12:04:55 +00:00
David Baker b867afe772 lint 2017-11-01 19:48:12 +00:00
David Baker e34fd89bb6 Expose more functionality of pushprocessor
This exposes a function to get rules by their ID and
ruleMatchesEvent to test whether a rule matches a given event.
2017-11-01 19:40:50 +00:00
Richard van der Hoff 51883b8f11 Merge pull request #555 from uhoreg/e2e_replay
keep track of event ID and timestamp of decrypted messages
2017-10-25 14:14:33 +01:00
David Baker ff5f95227a Merge pull request #564 from matrix-org/dbkr/fix_notifevents
Fix notifEvent computation
2017-10-25 12:06:05 +01:00
David Baker dc929236a4 oops, events not event 2017-10-25 12:03:17 +01:00
David Baker b09e20747a Oops, missing this 2017-10-25 12:00:11 +01:00
David Baker e8fc857dbc Don't calculate notifEvents until ready
We were previously computing notifEvents at the point where we
processed them but before the room they belong to was stored.
This was problematic because some push conditions try to get the
room and therefore failed. Since the push actions are cached, this
spurious calculation also got cached.

This moves the calculation out to a separate function that gets
called only after the room has been stored.
2017-10-25 11:54:44 +01:00
David Baker b42a13cc3b Merge pull request #563 from matrix-org/dbkr/fix_sentinels_on_power_levels
Fix power level of sentinel members
2017-10-25 11:47:18 +01:00
David Baker 37ed9800c5 Fix sender_notification_permission cond
Don't assume all events have a sender, because they don't.
2017-10-25 11:10:55 +01:00
David Baker 99f4968888 Replace all the sentinel members on power_levels
Rather than trying to be clever and keep ones whose power level isn't
affected, since they'll still have an associated power_levels event
that isn't the one actually in effect, which could be confusing.
2017-10-25 10:57:28 +01:00
David Baker 34428a811c Fix setPowerLevelEvent() semantics
* Make setPowerLevelEvent() use getDirectionalContent() rather than
   getContent(), otherwise power level events from pagination set
   power levels the wrong way
 * Don't assume the presence of a `users` key since the first
   power_levels event in a room will have no prev_content
2017-10-25 10:40:15 +01:00
Richard van der Hoff da7104b00d Merge pull request #554 from uhoreg/e2e_redact
don't try to decrypt a redacted message (fixes vector-im/riot-web#3744)
2017-10-24 21:02:01 +01:00
Hubert Chathi b373601e13 make sure redacted e2e messages are returned as a promise
Signed-off-by: Hubert Chathi <hubert@uhoreg.ca>
2017-10-24 13:11:48 -04:00
David Baker fcc90e884b Fix power level of sentinel members
When a ppower_levels event arrived, the current member objects were
being updated but the sentinel members were not, meaning that the
power levels of RoomMember objects associated with events had stale
power levels. This was causing power level based push rules to
be wrong.
2017-10-23 19:07:04 +01:00
David Baker 7a90096077 Merge pull request #562 from matrix-org/dbkr/room_notifs
Support room notifs
2017-10-23 17:01:23 +01:00
David Baker e7886a55fe lint 2017-10-23 16:50:01 +01:00
David Baker e33b786f65 Support room notifs
Specifically, add support for the sender_notification_permission
push rule condition. Also delays calculating notification events
until after they're added to the timeline, as per comment.
2017-10-23 16:02:06 +01:00
Luke Barnard b0b50f4ef9 Implement API wrapper for GET /group/$groupId/invited_users (#560) 2017-10-16 18:23:40 +02:00
David Baker 37c8e6dc07 Merge branch 'master' into develop 2017-10-16 14:37:39 +01:00
David Baker 675441fe93 v0.8.5 2017-10-16 14:35:20 +01:00
David Baker 2495d97862 Prepare changelog for v0.8.5 2017-10-16 14:35:20 +01:00
David Baker 70c998dd55 Merge pull request #558 from matrix-org/dbkr/fix_glob_to_regex
Fix the glob-to-regex code
2017-10-16 14:04:14 +01:00
David Baker ec60e46611 Merge remote-tracking branch 'origin/develop' into release-v0.8.5 2017-10-16 14:03:04 +01:00
David Baker bb2dc618f7 Merge pull request #559 from matrix-org/dbkr/unknown_condition_dont_match
Make unknown pushrule conditions not match
2017-10-16 14:02:02 +01:00
David Baker d9addf84ef Make unknown pushrule conditions not match
Unknown conditions previously were assumed to match, but given rules
can be added on the server as base rules, it's probably better to not
match unknown conditions.
2017-10-16 13:55:07 +01:00
David Baker d89163110e Fix the glob-to-regex code
It was only making one replacement so would fail for anything with
more than ine glob special char (eg. *foo would be fine, but *foo*
was broken)
2017-10-16 13:40:49 +01:00
Travis Ralston 7c851faba6 Support set_presence=offline for syncing
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-10-14 14:34:37 -06:00
Travis Ralston b70c219a05 Consider cases where the sender may not redact their own event
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-10-13 20:00:39 -06:00
David Baker 93b722af3c v0.8.5-rc.1 2017-10-13 11:01:58 +01:00
David Baker ee0e036aa5 Prepare changelog for v0.8.5-rc.1 2017-10-13 11:01:57 +01:00
David Baker 24ef004adc remove debug logging 2017-10-12 16:03:08 +01:00
Hubert Chathi dcab4eb70b fix lint error, and incorporate suggestions from richvdh and krombel
Signed-off-by: Hubert Chathi <hubert@uhoreg.ca>
2017-10-12 09:04:12 -04:00
Hubert Chathi 8f252992e4 keep track of event ID and timestamp of decrypted messages
This is to avoid false positives when detecting replay attacks.

fixes: vector-im/riot-web#3712

Signed-off-by: Hubert Chathi <hubert@uhoreg.ca>
2017-10-11 23:27:36 -04:00
Hubert Chathi a3a3e32e21 don't try to decrypt a redacted message (fixes vector-im/riot-web#3744)
set the decrypted data to an empty message

Signed-off-by: Hubert Chathi <hubert@uhoreg.ca>
2017-10-04 12:26:08 -04:00
David Baker f5f8867326 Merge pull request #553 from matrix-org/luke/groups-remove-room
Implement wrapper API for removing a room from a group
2017-10-04 10:58:28 +01:00
Luke Barnard 5494a9dd3b Implement wrapper API for removing a room from a group 2017-09-29 17:57:53 +01:00
David Baker 89f7857c84 Merge pull request #552 from matrix-org/rav/stuck_key_download
Fix typo which resulted in stuck key download requests
2017-09-29 10:22:26 +01:00
Richard van der Hoff 3140c6526a Fix typo which resulted in stuck key download requests
(... and hence stuck outgoing e2e messages)

Fixes https://github.com/vector-im/riot-web/issues/4278
2017-09-29 10:02:16 +01:00
Luke Barnard 96d828d22b Implement API wrapper for adding a room to a group (#551) 2017-09-27 14:27:19 +01:00
David Baker a8a50384b8 Add wrapper for group update_publicity API (#550)
Add wrapper for group update_publicity API
2017-09-25 15:27:59 +01:00
David Baker 787af6448d Merge pull request #549 from matrix-org/dbkr/groups_store_when_created
Store group when it's created
2017-09-22 18:02:17 +01:00
David Baker 4633135322 Store group when it's created
Otherwise we emit events when we set properties on the group but
getGroup() for that group returns null which is very confusing.
2017-09-22 17:57:08 +01:00
David Baker 050d522735 Merge pull request #548 from matrix-org/luke/groups-remove-rooms-users-from-summary
Luke/groups remove rooms users from summary
2017-09-22 16:23:47 +01:00
Luke Barnard 809b202e70 Merge branch 'develop' into luke/groups-remove-rooms-users-from-summary 2017-09-22 16:19:17 +01:00
Luke Barnard f1555dbb5d Implement wrapper APIs for removing users and rooms from group summary 2017-09-22 16:18:18 +01:00
krombel d1d0266a10 reduce sendToDevice payload (#522)
instead of sending one huge request split them up.
2017-09-22 14:29:23 +01:00
David Baker ee37ed0697 Add a clean target (#547) 2017-09-21 21:45:55 +01:00
David Baker e3972dee2c Merge branch 'master' into develop 2017-09-21 21:41:48 +01:00
David Baker 7a727e7eda v0.8.4 2017-09-21 21:39:37 +01:00
David Baker d24c822f68 Prepare changelog for v0.8.4 2017-09-21 21:39:37 +01:00
Richard van der Hoff 8d804013f3 Merge pull request #546 from matrix-org/dbkr/clean_on_prerelease
Clean on prepublish
2017-09-21 18:06:28 +01:00
David Baker d73a115436 Clean on prerelease
Otherwise you can make broken releases on case insensitive file
systems
2017-09-21 18:02:30 +01:00
Luke Barnard 0b4ff731e8 Implement wrapper APIs for adding rooms to group summary (#545) 2017-09-21 17:05:36 +01:00
Luke Barnard a58ec3f192 Implement wrapper APIs for adding rooms to group summary 2017-09-21 17:01:33 +01:00
Richard van der Hoff 868c20b161 Fix a race in decrypting megolm messages (#544)
* Fix a race in decrypting megolm messages

This fixes a race wherein it was possible for us to fail to decrypt a message,
if the keys arrived immediately after our attempt to decrypt it. In that case,
a retry *should* have been scheduled, but was not.

Fixes https://github.com/vector-im/riot-web/issues/5001.

* WORDS
2017-09-21 15:17:28 +01:00
Luke Barnard c2cd050419 Implement API to add users to group summaries (#543)
(and call it addUserToGroupSummary)
2017-09-20 16:31:28 +01:00
David Baker 7d5c107fb8 v0.8.3 2017-09-20 15:04:11 +01:00
David Baker d855a6ea0f Prepare changelog for v0.8.3 2017-09-20 15:04:10 +01:00
David Baker 464f84d8cd v0.8.3-rc.1 2017-09-19 10:44:58 +01:00
David Baker fe0ee6402a Prepare changelog for v0.8.3-rc.1 2017-09-19 10:44:57 +01:00
David Baker 068939f790 APIs for flair (#542) 2017-09-18 14:44:40 +01:00
Matthew Hodgson 35f48d1c8e Merge pull request #526 from t3chguy/allow_trailing_slash
consume trailing slash when creating Matrix Client in HS and IS urls
2017-09-17 22:53:48 +01:00
Matthew Hodgson 52adde2501 Merge pull request #539 from turt2live/travis/ignored_users
Add ignore users API
2017-09-17 21:47:03 +01:00
David Baker 0ddc4eceaf Merge pull request #540 from matrix-org/dbkr/jsdoc_355
Upgrade to jsdoc 3.5.5
2017-09-15 09:19:21 +01:00
turt2live b0ab8c750d Add isUserIgnored convenience method
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-09-14 19:47:16 -06:00
Travis Ralston b17dd8351f Make linter happy
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-09-14 19:24:18 -06:00
Travis Ralston 0ceb8d159a Generate the ignored users event content correctly
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-09-14 16:06:55 -06:00
David Baker 402b943ddb Upgrade to jsdoc 3.5.5
Because of https://github.com/jsdoc3/jsdoc/issues/1438
2017-09-14 22:33:10 +01:00
Travis Ralston be55451c90 Add ignore users API
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-09-14 12:55:48 -06:00
Matthew Hodgson c51c1a2ae6 Merge pull request #538 from matrix-org/dbkr/reemit_use_fewer_closures
Make re-emitting events much more memory efficient
2017-09-13 12:51:26 +01:00
David Baker 845c796b96 Make re-emitting events much more memory efficient
The previous impl bluntly created a closure for every event type
and source emitter we set up a re-emit for. We can do much better
than this fairly easily by having one bound handler for each event
name and moving it into a class so we have one emitter per target,
since 99% of the time the target is the client object.
2017-09-13 11:55:03 +01:00
David Baker b0918ef293 Merge pull request #536 from matrix-org/dbkr/only_reemit_events_when_needed
Only re-emit events from Event objects if needed
2017-09-12 19:26:58 +01:00
David Baker 102572b088 Empty commit to get Travis to re-test 2017-09-12 17:37:16 +01:00
David Baker 63076e77f5 Only re-emit events from Event objects if needed
The only event and Event emits is 'Event.decrypted', so don't
bother to add listeners if the event isn't encrypted.
2017-09-12 17:09:01 +01:00
Richard van der Hoff 8e48ee5f66 Merge pull request #535 from matrix-org/rav/devicelist_bits
Handle 'left' users in the deviceList mananagement
2017-09-08 16:21:58 +01:00
Richard van der Hoff 1a55f550c0 Handle 'left' users in the deviceList mananagement
When we no longer share any rooms with a given user, the server will stop
sending us updates on their device list, and will (once synapse is updated)
send us a notification of that fact via the 'left' field in the device_lists
field in /sync, or the response from /keys/changes.
2017-09-08 15:40:26 +01:00
Richard van der Hoff ae8fc64394 Do /keys/changes before second /sync
This will avoid races between /keys/changes and /syncs.
2017-09-08 15:40:26 +01:00
David Baker 5e8e56caf9 Merge pull request #534 from matrix-org/rav/factor_out_device_tests
Factor out devicelist integration tests to a separate file
2017-09-08 13:51:11 +01:00
David Baker c075c161c2 Merge pull request #533 from matrix-org/rav/refactor_sync
Refactor sync._sync as an async function
2017-09-08 13:51:05 +01:00
Richard van der Hoff 237a553d15 sync: s/self/this/ 2017-09-08 12:07:19 +01:00
Richard van der Hoff ca8674e0de Factor out devicelist integration tests to a separate file
There's a fuzzy line between the megolm tests and the devicelist ones, but
since I want to add more tests for devicelists, we might as well put the ones
which are definitely about devicelists in their own file
2017-09-08 11:54:16 +01:00
Richard van der Hoff 0511a1172f Refactor sync._sync as an async function
This stuff is much more clearly expressed as an async function than as a
promise chain, and I'm about to add more stuff to the chain, so let's refactor
now.

There is plenty more that could be done here (self -> this, for instance), but
I've gone for keeping the diff minimal.
2017-09-08 11:53:10 +01:00
David Baker e07b304914 Merge pull request #532 from matrix-org/rav/eslint_es6
Add es6 to eslint environments
2017-09-07 10:33:52 +01:00
Richard van der Hoff 17364e72ec Add es6 to eslint environments 2017-09-07 10:29:27 +01:00
David Baker 22b213ae26 v0.8.2 2017-08-24 14:45:24 +01:00
David Baker 7d5936a9e9 Prepare changelog for v0.8.2 2017-08-24 14:45:24 +01:00
David Baker ab8f466f53 Merge pull request #530 from matrix-org/rav/fix_encrypted_calls
Handle m.call.* events which are decrypted asynchronously
2017-08-24 14:09:12 +01:00
David Baker 201177e7f0 Merge pull request #529 from matrix-org/dbkr/event_object_reemit
Re-emit events from, er, Event objects
2017-08-24 13:35:30 +01:00
Richard van der Hoff ec5f9a2892 Handle m.call.* events which are decrypted asynchronously
Handle the case where received m.call.* events are not decrypted at the point
of the 'event' notification by adding an 'Event.decrypted' listener for them.
2017-08-24 13:35:02 +01:00
Richard van der Hoff ee5b8748b5 Add MatrixEvent.isDecryptionFailure() 2017-08-24 13:35:02 +01:00
David Baker 8d04f8b8b5 Re-emit events from, er, Event objects
We do create Events in more places, but this is probably the only
place that matters since the only event is 'decrypted' which won't
fire for, eg. events we send.
2017-08-24 11:29:48 +01:00
David Baker 033babfbfc Groups: Sync Stream, Accept Invite & Leave (#528)
* WIP support for reading groups from sync stream

Only does invites currently

* More support for parsing groups in the sync stream

* Fix jsdoc
2017-08-24 10:24:24 +01:00
David Baker 15b77861ea v0.8.1 2017-08-23 15:51:31 +01:00
David Baker c4721850ce Prepare changelog for v0.8.1 2017-08-23 15:51:31 +01:00
David Baker b325aad5c9 v0.8.1-rc.1 2017-08-22 18:40:29 +01:00
David Baker 92e616f18e Prepare changelog for v0.8.1-rc.1 2017-08-22 18:40:29 +01:00
David Baker f7fee29c76 Merge pull request #527 from matrix-org/rav/fix_interactive_auth_error_handling
Fix error handling in interactive-auth
2017-08-21 16:43:19 +01:00
Richard van der Hoff eccea7411f Fix error handling in interactive-auth
Now that we are using bluebird, `defer.reject` is not implicitly bound, so we
need to call it properly rather than just passing it into the catch handler.

This fixes an error:

   promise.js:711 Uncaught TypeError: Cannot read property 'promise' of undefined
2017-08-21 16:31:42 +01:00
Michael Telatynski 01f93e0970 consume trailing slash when creating Matrix Client in HS and IS urls
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-08-19 11:00:46 +01:00
Richard van der Hoff 2d82a7bc2e Merge pull request #524 from matrix-org/rav/async_crypto/1
Make lots of OlmDevice asynchronous
2017-08-17 13:16:57 +01:00
Richard van der Hoff ca91fba071 Crypto test: Bump the timeout when waiting for Ali to claim keys
This failed a test, so let's just bump up the timeout a bit more.
2017-08-16 21:02:52 +01:00
Richard van der Hoff 9f2fce4d87 Try harder to wait for megolm decryption
Ok, this *really* ought to fix the racy test.
2017-08-16 19:01:47 +01:00
David Baker e1942267c5 Add API to invite & remove users from groups (#525)
* Add API invite & remove users from groups

* lint
2017-08-16 14:45:15 +01:00
Richard van der Hoff 12212409c7 Hopefully, fix racy megolm test
I couldn't repro the failure locally, but this looks like it should fix the
test failures.
2017-08-15 19:09:50 +01:00
Richard van der Hoff e5565c6bdb review comments 2017-08-15 18:34:04 +01:00
Richard van der Hoff f00558d840 Merge remote-tracking branch 'origin/develop' into rav/async_crypto/1 2017-08-15 18:31:02 +01:00
Richard van der Hoff da0dc5ed11 Merge pull request #523 from matrix-org/rav/fix_decryption_race
Make crypto.decryptMessage return decryption results
2017-08-15 18:07:33 +01:00
Luke Barnard b417492fad v0.8.0 2017-08-15 17:11:05 +01:00
Luke Barnard d3ee532624 Prepare changelog for v0.8.0 2017-08-15 17:11:05 +01:00
Richard van der Hoff e8be38ce5a Add delays to tests to wait for things to decrypt
Prepare for some refactoring which will add an extra tick to decryption by
adding some `awaitDecryption` calls in the integration tests.
2017-08-14 18:39:45 +01:00
Richard van der Hoff 38c9a05a0c Make Event.attemptDecryption return useful promises
Even if a decryption attempt is in progress, return a promise which blocks
until the attempt is complete.
2017-08-14 18:38:29 +01:00
Richard van der Hoff 110bd332f4 Make OlmDevice.exportInboundGroupSession async 2017-08-10 15:01:56 +01:00
Richard van der Hoff 8a0f73bf81 Make some OlmDevice megolm methods async
* OlmDevice.hasInboundSessionKeys
* OlmDevice.getInboundGroupSessionKey

The latter means that MegolmDecryption.shareKeysWithDevice takes longer before
it sends out the keyshare, so means the unit test needed an update
2017-08-10 15:01:56 +01:00
Richard van der Hoff 337c9cbea3 Make OlmDevice.decryptGroupMessage async 2017-08-10 15:01:56 +01:00
Richard van der Hoff cfd61096d9 Make OlmDevice.importInboundGroupSession async 2017-08-10 15:01:56 +01:00
Richard van der Hoff 2894e253a2 Make OlmDevice.addInboundGroupSession async 2017-08-10 15:01:56 +01:00
Richard van der Hoff e52985e082 Olm session creation async 2017-08-10 15:01:56 +01:00
Richard van der Hoff 7d2bc12bb7 Make OlmDevice key generation async
* OlmDevice.generateOneTimeKeys becomes async
* Stash maxOneTimeKeys at init so that maxNumberOfOneTimeKeys can remain sync
2017-08-10 15:01:56 +01:00
Richard van der Hoff a5f397b26d OlmDevice.oneTimeKeys async
* OlmDevice.getOneTimeKeys
* OlmDevice.markKeysAsPublished
2017-08-10 15:01:56 +01:00
Richard van der Hoff 5b93d5210e Make OlmDevice.sign async 2017-08-10 15:01:56 +01:00
Richard van der Hoff e943a6e09c Make OlmDevice olmSession methods asynchronous
* OlmDevice.encryptMessage
* OlmDevice.decryptMessage
* OlmDevice.matchesSession
2017-08-10 15:01:56 +01:00
Richard van der Hoff 8f527a6212 make session tracking methods in OlmDevice async
* OlmDevice.getSessionIdsForDevice
* OlmDevice.getSessionIdForDevice
* OlmDevice.getSessionInfoForDevice
2017-08-10 15:01:56 +01:00
Richard van der Hoff f2f8ad6b65 Make OlmDevice initialisation asynchronous
Add an asynchronous `init` method to OlmDevice which initialises the OlmAccount.
2017-08-10 15:01:56 +01:00
Richard van der Hoff c870930bc0 Add delays to tests to wait for things to decrypt
Prepare for some refactoring which will add an extra tick to decryption by
adding some `awaitDecryption` calls in the integration tests.
2017-08-10 15:01:56 +01:00
Richard van der Hoff b26b1caa86 fix jsdoc 2017-08-10 14:56:42 +01:00
Richard van der Hoff 6613ee6b0d Make crypto.decryptMessage return decryption results
... instead of having it call event.setClearData.

The main advantage of this is that it fixes a race condition, wherein apps
could see `event.isDecrypting()` to be true, but in fact the event had been
decrypted (and there was no `Event.decrypted` event on its way).

We're also fixing another race, wherein if the first attempt to decrypt failed,
a call to `attemptDecryption` would race against the first call and a second
attempt to decrypt would never happen.

This also gives a cleaner interface to MatrixEvent, at the expense of making
the `megolm` unit test a bit more hoop-jumpy.
2017-08-10 13:05:35 +01:00
Richard van der Hoff 9550bca099 Megolm: remove redundant requestKeysOnFail
We now *always* requestKeysOnFail, so this was dead code which we can remove.
2017-08-10 13:00:27 +01:00
Richard van der Hoff 92a75aaa08 Merge pull request #521 from matrix-org/rav/async_crypto/olmlib
Make bits of `olmlib` asynchronous
2017-08-10 11:07:42 +01:00
Richard van der Hoff 906bf88450 Merge remote-tracking branch 'origin/develop' into rav/async_crypto/olmlib 2017-08-09 18:11:48 +01:00
Richard van der Hoff d7157843f4 Merge pull request #520 from matrix-org/rav/async_crypto/devicelist
Make some of DeviceList asynchronous
2017-08-09 18:02:53 +01:00
Richard van der Hoff d317c1ff08 Merge pull request #519 from matrix-org/rav/async_crypto/algorithms
Make methods in crypto/algorithms async
2017-08-09 18:02:30 +01:00
Richard van der Hoff ef889963d9 Rewrite olmlib.ensureOlmSessionsForDevices as async
This is non-functional. It just looks a lot prettier.
2017-08-09 10:46:19 +01:00
Richard van der Hoff a2d7b221ee Make olmlib.verifySignature async 2017-08-09 10:46:18 +01:00
Richard van der Hoff aff32afefa Make olmlib.encryptMessageForDevice async 2017-08-09 10:46:18 +01:00
Richard van der Hoff 0943e0c60f Make some of DeviceList asynchronous
* DeviceList._updateStoredDeviceKeysForUser
 * DeviceList._processQueryResponseForUser
 * DeviceList._storeDeviceKeys
2017-08-08 18:28:53 +01:00
Richard van der Hoff 18f75ec61c make algorithm.hasKeysForKeyRequest async 2017-08-08 18:26:31 +01:00
Richard van der Hoff d821082843 Prepare megolm.js for async
Make internal methods of megolm.js ready for asynchronous olmdevice
2017-08-08 18:25:16 +01:00
Richard van der Hoff 366a88cc5c make olm._decryptMessage asynchronous 2017-08-08 18:22:55 +01:00
David Baker 951df61aa0 Merge pull request #518 from matrix-org/rav/no_plain_messages_in_e2e_room
Avoid sending unencrypted messages in e2e room
2017-08-08 12:48:49 +01:00
Richard van der Hoff 3e79575602 Avoid sending unencrypted messages in e2e room
Reshuffle the logic for determining whether to encrypt a message so that it can
run independently of whether our app actually supports e2e - and then throw an
error if it looks like we should be encrypting but don't support it.

This seems a preferable situation to just falling back to plain text if we get
a dodgy build.
2017-08-08 12:29:26 +01:00
David Baker 92e24777c0 Merge pull request #517 from matrix-org/rav/test_robustness
Make tests wait for syncs to happen
2017-08-08 11:27:13 +01:00
Richard van der Hoff ab8d06bb86 Make tests wait for syncs to happen
Add lots of calls to `syncPromise` to cope with the fact that sync responses
are now handled asynchronously, which makes them prone to races otherwise.

Also a quick sanity-check in crypto to make one of the test failures less
cryptic.
2017-08-08 10:58:19 +01:00
Richard van der Hoff 8563dd5860 Merge pull request #510 from matrix-org/rav/async_crypto/crypto_methods
Make a load of methods in the 'Crypto' module asynchronous
2017-08-07 17:14:07 +01:00
Richard van der Hoff 1f6153fa82 Make Crypto.setRoomEncryption asynchronous 2017-08-07 17:13:09 +01:00
Richard van der Hoff 23d66b9746 Make Crypto.setDeviceVerification async 2017-08-07 17:13:09 +01:00
Richard van der Hoff 25ccd6bc6d Make Crypto._processReceivedRoomKeyRequests async
This is slightly complicated by the fact that it's initiated from a synchronous
process which we don't want to make async (processing the /sync response) and
we want to avoid racing two copies of the processor.
2017-08-07 17:13:09 +01:00
Richard van der Hoff 14ad32bcd2 Make Crypto.importRoomKeys async 2017-08-07 17:13:09 +01:00
Richard van der Hoff 9ab9b9d75a Make crypto._signObject async 2017-08-07 17:13:09 +01:00
Richard van der Hoff b7a3c4557f Make crypto.getOlmSessionsForUser async
This is snever used anywhere (it's mosdly for debug), so this is trivial
2017-08-07 17:13:08 +01:00
David Baker bdb90b4b33 Merge pull request #515 from matrix-org/luke/fix-null-rawDisplayName
Set `rawDisplayName` to `userId` if membership has `displayname=null`
2017-07-27 16:23:31 +01:00
Luke Barnard 85d0935e97 Set rawDisplayName to userId if membership has displayname=null
This mirrors the behaviour of `name` such that the default is always `userId` but if the membership event has a `displayname`, we use that.
2017-07-27 16:15:32 +01:00
Richard van der Hoff 86ad75d27b Merge pull request #508 from matrix-org/rav/async_crypto_event_handling
Refactor handling of crypto events for async
2017-07-26 09:07:59 +01:00
Richard van der Hoff b40473aa3b Fix broken event-emitter test
We need to wait for two syncs, not just one, here.
2017-07-26 07:27:08 +01:00
Richard van der Hoff 3bd5ffc5cd Fix broken crypto test
Now that sync takes a bit longer to send out Event events, the encrypted events
have already been decrypted by the time the test sees them - so we no longer
need to await their decryption.
2017-07-26 07:20:02 +01:00
Richard van der Hoff 10aafd3738 Merge branch 'develop' into rav/async_crypto_event_handling 2017-07-26 07:11:48 +01:00
Richard van der Hoff c055765bfe Merge pull request #509 from rav/async_crypto/async_decryption 2017-07-26 07:09:12 +01:00
Richard van der Hoff d8f486fc0d Verbose logging to see what's up with indexeddb (#514)
In an attempt to see why our tests sometimes time out, add a load of logging to
confirm exactly where it is happening.
2017-07-25 11:38:27 +01:00
Luke Barnard 06eea71a37 Add rawDisplayName to RoomMember (#513)
* Add rawDisplayName to RoomMember

This will at first be the `userId`, but when the members membership event is set, `rawDisplayName` will be assigned to the raw `displayname` of the membership event. This deliberately avoids disambiguation so that clients can disambiguate themselves (via a tooltip or otherwise).

* Clarify docs
2017-07-24 17:35:53 +01:00
Richard van der Hoff 3effb9ec29 Merge pull request #511 from matrix-org/rav/async_to_bluebird
Transform `async` functions to bluebird promises
2017-07-24 10:26:26 +01:00
David Baker 6603a2300b Merge pull request #512 from matrix-org/dbkr/groupview_edit
Add more group APIs
2017-07-24 10:00:06 +01:00
David Baker ed029fe348 More useful doc 2017-07-24 09:55:04 +01:00
Richard van der Hoff b497bc5eb9 Fix broken test: wait for sync to complete 2017-07-21 16:04:53 +01:00
Richard van der Hoff 8a4a1dfadf Transform async functions to bluebird promises
Now that we use transform-runtime instead of regenerator-runtime, we need to
use the async-to-bluebird transform to make sure that `async` functions get
transformed into bluebird promises.
2017-07-21 15:59:30 +01:00
Richard van der Hoff 8bbf14acbf Let event decryption be asynchronous
Once everything moves to indexeddb, it's going to require callbacks and the
like, so let's make the decrypt API asynchronous in preparation.
2017-07-21 14:41:22 +01:00
Richard van der Hoff 86f2c86440 Add MatrixEvent.attemptDecryption
... and use it from both MatrixClient and the megolm re-decryption code.

This will help us avoid races when decryption is asynchronous.
2017-07-21 14:41:22 +01:00
Richard van der Hoff cfb29f1339 Refactor handling of crypto events for async
We're going to need to handle m.room.crypto events asynchronously, so
restructure the way we do that.
2017-07-21 14:41:01 +01:00
Richard van der Hoff 63a28d8e34 Fix lint in /sync 2017-07-21 14:41:01 +01:00
Richard van der Hoff d37cbb10a5 Merge pull request #507 from matrix-org/rav/fix_racy_cancellation_test
Retrying test: wait for localEchoUpdated event
2017-07-21 13:19:55 +01:00
David Baker 055590c0c6 Add more group APIs 2017-07-21 11:13:27 +01:00
David Baker 6aaac45468 Merge pull request #504 from matrix-org/dbkr/fix_member_events_timeline_reset_2
Fix member events breaking on timeline reset, 2
2017-07-20 14:25:41 +01:00
David Baker 34adaae5af Add helpfully named variable for old timeline 2017-07-20 14:17:14 +01:00
Richard van der Hoff 9c6f004f7f Retrying test: wait for localEchoUpdated event
We need to wait for the js-sdk to have an opportunity to process the 400 from
the /send/ request before checking the event state
2017-07-20 13:12:21 +01:00
David Baker ff685e33d5 clarify comment 2017-07-20 11:00:50 +01:00
David Baker 2999603b28 more commentage 2017-07-20 10:43:47 +01:00
Richard van der Hoff 32d8f4b084 Fix jsdoc failure on async code (#506)
We need jsdoc 3.5 to support the async/await syntax.
2017-07-20 09:45:37 +01:00
Richard van der Hoff 0fb0c1b71b Use babel transform-runtime instead of regenerator-runtime (#505)
Attempting to use the regnerator-runtime ourselves led to a fight with riot-web
about whether `global.regeneratorRuntime` could be defined. By using the
transform-runtime plugin, references to `global.regeneratorRuntime` which are
created by the transform-regenerator plugin are turned into references to an
imported module, which works much better.

(The full tragic tale went as follows:

- riot-web uses transform-runtime, which adds an import of
  `regenerator-runtime` to index.js
- `regenerator-runtime`:
   - loads `regenerator-runtime/runtime`, which defines
     `global.regeneratorRuntime`
   - then clears the global property and returns the regeneratorRuntime object
     as the exported value from the module
- later, the js-sdk tried to import `regenerator-runtime/runtime`, which then
  did nothing because the module had already been loaded once.

For added fun, this only manifested itself when riot-web and js-sdk shared an
instance of the `regenerator-runtime` package, which happens on proper builds,
but not a normal development setup.)
2017-07-20 09:18:37 +01:00
Richard van der Hoff 2ac34dbab0 Merge pull request #503 from matrix-org/rav/async_crypto/public_api
Make bits of the js-sdk api asynchronous
2017-07-19 21:14:40 +01:00
Richard van der Hoff 986fb12543 Fix some typos in comments 2017-07-19 21:13:06 +01:00
krombel e686eb750f use device_one_time_keys_count transmitted by /sync (#493)
Where it is available, use the one_time_keys_count returned by /sync instead of polling the server for it.

This was added to synapse in matrix-org/synapse#2237.
2017-07-19 16:27:05 +01:00
David Baker 8ac15068ee more comments 2017-07-19 16:24:42 +01:00
David Baker 5e4cd6cf11 Add hopefully clearer comments 2017-07-19 16:20:40 +01:00
David Baker 39d694de8c No longer need RoomState 2017-07-19 14:58:18 +01:00
David Baker 342f5c01e0 Update tests for new resetLiveTimeline interface 2017-07-19 14:54:18 +01:00
David Baker f91293c6c5 Set the start state of the new timeline correctly 2017-07-19 14:52:27 +01:00
David Baker 1ce4977a70 get state events before we nuke the roomstate 2017-07-19 12:00:04 +01:00
David Baker f4b25b59e5 Lint 2017-07-19 11:56:58 +01:00
David Baker b33a47e253 Fix member events breaking on timeline reset, 2
Re-use the same RoomState from the old live timeline so we re-use
all the same member objects etc, so all the listeners stay attached
2017-07-19 11:49:20 +01:00
Richard van der Hoff ccd4d4263d Changelog: breaking e2e changes 2017-07-18 23:35:33 +01:00
Richard van der Hoff 2ff9a36eed Make a number of the crypto APIs asynchronous
Make the following return Promises:

* `MatrixClient.getStoredDevicesForUser`
* `MatrixClient.getStoredDevice`
* `MatrixClient.setDeviceVerified`
* `MatrixClient.setDeviceBlocked`
* `MatrixClient.setDeviceKnown`
* `MatrixClient.getEventSenderDeviceInfo`
* `MatrixClient.isEventSenderVerified`
* `MatrixClient.importRoomKeys`

Remove `listDeviceKeys` altogether: it's been deprecated for ages, and since
applications are going to have to be changed anyway, they might as well use its
replacement (`getStoredDevices`).
2017-07-18 23:35:33 +01:00
Richard van der Hoff d1e91cd702 Add MatrixClient.initCrypto
initialising the crypto layer needs to become asynchronous. Rather than making
`sdk.createClient` asynchronous, which would break every single app in the
world, add `initCrypto`, which will only break those attempting to do e2e (and
in a way which will fall back to only supporting unencrypted events).
2017-07-18 23:35:33 +01:00
Richard van der Hoff e2599071c5 Merge pull request #499 from matrix-org/rav/omfg_when_will_it_end
Yet more js-sdk test deflakification
2017-07-17 11:57:05 +01:00
Richard van der Hoff fc38b89aee Merge pull request #497 from matrix-org/rav/yet_another_flakey_test
Fix racy 'matrixclient retrying' test
2017-07-17 11:56:39 +01:00
Richard van der Hoff 5688286a79 Merge pull request #495 from matrix-org/rav/fix_key_requests_race
Fix spamming of key-share-requests
2017-07-17 11:56:05 +01:00
David Baker 7eb10ab7ac Merge pull request #500 from matrix-org/rav/upload_progress
Add progress handler to `uploadContent`
2017-07-14 17:18:41 +01:00
Richard van der Hoff 34b31865c5 Add progress handler to uploadContent
bluebird doesn't support promise progression (or rather, it does, but it's
heavily deprecated and doesn't use the same API as q), so replace the
(undocumented) promise progression on uploadFile with a callback.
2017-07-14 16:51:43 +01:00
Richard van der Hoff f1c5b632cc Deflake the matrixclient syncing tests (#498)
All of these tests were vulnerable to a race wherein we would flush the /sync
request, but the client had not yet processed the results before we checked
them. We can solve all of this by waiting for the client to emit a "sync"
event.
2017-07-14 16:09:28 +01:00
Richard van der Hoff 04ca0ac2b5 Give the megolm tests longer to complete
All that crypto stuff takes a while, so give it longer than 100ms.
2017-07-14 15:22:08 +01:00
Richard van der Hoff 8b2fdf3a75 Deflake megolm unit test
Waiting for 1ms isn't actually good enough. wait for the actual thing we are
actually waiting for.
2017-07-14 15:22:08 +01:00
Richard van der Hoff adca75b7d8 Deflake matrix-client-timeline tests
These guys do a flush("/sync"), without waiting for it to complete, and then in
the afterEach, check that the sync has been flushed, which it may not have
been. So we should make sure we wait for the flush.
2017-07-14 15:22:08 +01:00
Richard van der Hoff 504fa2a1d3 Fix racy 'matrixclient retrying' test
when a message send fails, the promise returned by `sendMessage` is
rejected. Until we switched to bluebird, the rejection was happily being
swallowed, but with bluebird, there's a better chance of the unhandled
rejection being caught by the runtime and mocha and failing the test.
2017-07-13 18:18:21 +01:00
Richard van der Hoff 266a062a5d Fix spamming of key-share-requests
Fixes a race in the memory-backed crypto store which meant that we would spam
out multiple key-share-requests for the same session.

(This didn't happen very often in practice, because normally we use the
indexeddb-backed store, which is race-free. Or at least, doesn't have this
race.)
2017-07-13 13:29:56 +01:00
Richard van der Hoff 652a9452c2 Merge pull request #490 from matrix-org/rav/bluebird
Switch matrix-js-sdk to bluebird
2017-07-12 23:34:40 +01:00
Richard van der Hoff 503b6ea6c8 Correct incorrect Promise() invocation
you're supposed to call Promise() as a constructor rather than a static
function.
2017-07-12 23:33:55 +01:00
Richard van der Hoff 547501ba81 Replace promise.inspect()
Bluebird promises don't have an `inspect()` method, but do have an
`isFulfilled()` and a `value()` method, so use them instead.
2017-07-12 23:33:55 +01:00
Richard van der Hoff cfffbc4a09 replace q method calls with bluebird ones
```
find src spec -name '*.js' |
    xargs perl -i -pe 's/q\.(all|defer|reject|delay|try)\(/Promise.$1(/'
```
2017-07-12 23:33:55 +01:00
Richard van der Hoff b58d84fba1 q.Promise -> Promise
```
find src spec -name '*.js' |
    xargs perl -i -pe 's/q\.Promise/Promise/'
```
2017-07-12 23:32:28 +01:00
Richard van der Hoff a5d3dd942e q(...) -> Promise.resolve
```
find src spec -name '*.js' |
    xargs perl -i -pe 's/\bq(\([^(]*\))/Promise.resolve$1/'
```
2017-07-12 23:32:28 +01:00
Richard van der Hoff b96062b6de replace imports of q with bluebird
```
find src spec -name '*.js' |
   xargs perl -i -pe 'if (/require\(.q.\)/) { $_ = "import Promise from '\''bluebird'\'';\n"; }'

find src spec -name '*.js' |
   xargs perl -i -pe 'if (/import q/) { $_ = "import Promise from '\''bluebird'\'';\n"; }'
```
2017-07-12 23:32:28 +01:00
Richard van der Hoff 04b71c11e1 Merge pull request #492 from matrix-org/rav/even_more_flakey_tests
Fix some more flakey tests
2017-07-12 18:17:16 +01:00
Richard van der Hoff 651baefb1d Remove redundant expectations
Apparently we weren't hitting these expected requests, so let's get rid of them.
2017-07-12 17:25:59 +01:00
Richard van der Hoff ff7e845615 remove redundant flushAllExpected
Turned out this flush was completely redundant
2017-07-12 17:23:11 +01:00
Richard van der Hoff f0612a1407 Fix some more flakey tests
switch a bunch of `flush()`es to `flushAllExpected()`s
2017-07-12 16:28:21 +01:00
Richard van der Hoff 83bd24adf8 More test deflakifying (#491)
Call `flushAllExpected()` from some more places. In a couple of places, we were
apparently calling `flush()` redundantly, so remove it altogether.
2017-07-12 14:05:39 +01:00
Richard van der Hoff b5a8e6bbdf Merge pull request #489 from t3chguy/t3chguy/test-crossplatform
make the npm test script windows-friendly
2017-07-12 13:49:43 +01:00
Michael Telatynski 9798fcf839 make the npm test script windows-friendly 2017-07-11 23:08:28 +01:00
David Baker 15556b6797 Merge pull request #488 from matrix-org/rav/deflakify_tests
Fix a bunch of races in the tests
2017-07-11 13:22:27 +01:00
Richard van der Hoff 0ca4d728d8 Fix a bunch of races in the tests
Once we switch to bluebird, suddenly a load of timing issues come out of the
woodwork. Basically, we need to try harder when flushing requests. Bump to
matrix-mock-request 1.1.0, which provides `flushAllExpected`, and waits for
requests to arrive when given a `numToFlush`; then use `flushAllExpected` in
various places to make the tests more resilient.
2017-07-11 12:09:21 +01:00
David Baker b2c7804032 Merge pull request #487 from matrix-org/rav/fix_bad_all_usage
Fix early return in MatrixClient.setGuestAccess
2017-07-11 11:21:45 +01:00
David Baker e091dc0294 Merge pull request #486 from matrix-org/rav/kill_failTest
Remove testUtils.failTest
2017-07-11 11:21:14 +01:00
Richard van der Hoff 3bfb4595cf Remove redundant calls to done
These tests which return a promise already don't need to call `done`.
2017-07-10 17:40:23 +01:00
Richard van der Hoff 8955d8de23 remove utils.failTest
this is no longer used, so kill it
2017-07-10 17:25:56 +01:00
Richard van der Hoff 1372b298bb kill off more utils.failTest refs
manual replacement of some more complicated utils.failTest usages with q.all()
invocations.
2017-07-10 17:25:48 +01:00
Richard van der Hoff 9558845e6e Fix early return in MatrixClient.setGuestAccess
(as well as a similar bug in the test suite)

Turns out that `q.all(a, b)` === `q.all([a])`, rather than `q.all([a,b])`: it
only waits for the *first* promise - which means that `client.setGuestAccess`
would swallow any errors returned from the API.
2017-07-10 17:14:52 +01:00
Richard van der Hoff 5ab0930de8 utils.failTest -> nodeify
Automated replacement of utils.failTest with nodeify

This was done with the perl incantation:

```
    find spec -name '*.js' |
        xargs perl -i -pe 's/catch\((testUtils|utils).failTest\).done\(done\)/nodeify(done)/'
```

more auto
2017-07-10 16:37:31 +01:00
David Baker 3294f4858a Merge pull request #485 from matrix-org/rav/test_watch
Add test:watch script
2017-07-07 14:25:36 +01:00
Richard van der Hoff eea9a3ba59 Add test:watch script
... to run the tests in a loop.
2017-07-07 14:19:41 +01:00
Richard van der Hoff 753974d663 Merge pull request #484 from matrix-org/rav/enable_async
Make it possible to use async/await
2017-07-07 13:47:59 +01:00
David Baker 527cd0a6e5 Implement 'joined_groups' API (#477)
* Add group summary api

* Add doc for group summary API

and remove callback param as it's deprecated

* API for /joined_groups

* Create group API

* Make doc marginally more helpful
2017-07-06 22:02:17 +01:00
Richard van der Hoff 24f70387d2 Make it possible to use async/await
Enables the babel plugin that transpiles async/await to generator functions,
and load the regenerator runtime so that generator functions work.
2017-07-06 18:52:37 +01:00
Richard van der Hoff adc2070ac1 Merge pull request #483 from matrix-org/rav/remove_new_device_support
Remove m.new_device support
2017-07-06 17:10:37 +01:00
Richard van der Hoff a8642682d0 Remove m.new_device support
We now rely on the server to track new devices, and tell us about them when
users add them, rather than forcing devices to announce themselves (see
https://github.com/vector-im/riot-web/issues/2305 for the whole backstory
there).

The necessary support for that has now been in all the clients and the server
for several months (since March or so). I now want to get rid of the
localstorage store, which this code is relying on, so now seems like a good
time to get rid of it. Yay.
2017-07-06 16:05:40 +01:00
Kegsay d66e6db480 Merge pull request #478 from krombel/access_token_header
Use access-token in header
2017-07-06 13:44:31 +01:00
Krombel dc66bbc3dc pass useAuthorizationHeader from constructor; add docs 2017-07-06 13:47:54 +02:00
Krombel 6e7f5feea5 remove fallback to query-params and set Authorization-Header based on construcor-option 2017-07-05 17:04:40 +02:00
Richard van der Hoff f21ea6c065 Extend timeout in megolm test
Use the default timeout of 100ms when waiting for the /send request, instead of
clamping it to 20ms.
2017-07-05 15:08:50 +01:00
Richard van der Hoff 6af56b56bc Merge pull request #482 from matrix-org/rav/sanity_check_protocols
Sanity-check response from /thirdparty/protocols
2017-07-05 11:09:20 +01:00
Richard van der Hoff 598d40b0b7 Sanity-check response from /thirdparty/protocols
Check that /thirdparty/protocols gives us an object (rather than a string, for
instance). I saw a test explode, apparently because it gave us a string. Which
is odd, but in general we ought to be sanity-checking the things coming back
from the server.
2017-07-05 10:51:08 +01:00
Richard van der Hoff 6ae714f51f Merge pull request #479 from matrix-org/rav/error_parsing
Avoid parsing plain-text errors as JSON
2017-07-04 17:11:47 +01:00
Richard van der Hoff b0661bb586 Update to matrix-mock-request 1.0
-- to pick up on the json parsing differences
2017-07-04 16:35:33 +01:00
Richard van der Hoff b6a165f1f8 Merge branch 'develop' into rav/error_parsing 2017-07-04 16:03:33 +01:00
Richard van der Hoff 8fe4a36b68 Merge pull request #481 from matrix-org/rav/use_external_mock_request
Use external mock-request
2017-07-04 16:01:21 +01:00
Richard van der Hoff 0d24f2d4c1 Use external mock-request
mock-request is now factored out to matrix-mock-request; use it
2017-07-04 15:45:22 +01:00
Krombel dd0ff3eeb5 intercept first authedRequest to determine if accessToken can be send by header (clearer structure) 2017-07-04 16:16:10 +02:00
Krombel 07868f701a Merge remote-tracking branch 'upstream/develop' into access_token_header 2017-07-04 15:28:17 +02:00
Richard van der Hoff f4f0e4b60f Merge pull request #480 from matrix-org/rav/fix_test_races
Fix some races in the tests
2017-07-04 14:04:24 +01:00
Richard van der Hoff ae950a2ff4 Fix some races in the tests
There is a common pattern in the tests which is, when we want to mock a /sync,
to flush it, and then, in the next tick of the promise loop, to wait for the
syncing event. However, this is racy: there is no guarantee that the syncing
event will not happen before the next tick of the promise loop.

Instead, we should set the expectation of the syncing event, then do the flush.
(Technically we only need to wait for the syncing event, but by waiting for
both we'll catch any errors thrown by the flush, and make sure we don't have
any outstanding flushes before proceeding).

Add a utility method to TestClient to do the above, and use it where we have a
TestClient.

(Also fixes a couple of other minor buglets in the tests).
2017-07-04 13:48:26 +01:00
Richard van der Hoff 5f6e4bdfe9 Avoid parsing plain-text errors as JSON
It's somewhat unhelpful to spam over the actual error from the reverse-proxy or
whatever with a SyntaxError.
2017-07-03 19:30:23 +01:00
Krombel c6d2d4ccda readd failover if server does not handle access-token via header 2017-07-01 14:30:37 +02:00
Krombel 59160a5d42 Implement failover when server does not allow setting the Authorized-header (CORS) 2017-07-01 12:16:46 +02:00
Krombel 5da6423fd6 Added failover if server does not recognize the auth header 2017-06-27 13:29:08 +02:00
Krombel d36b8721ca Allow Authorization-Header in tests 2017-06-23 15:49:07 +02:00
Krombel 539abffe0e Merge remote-tracking branch 'upstream/develop' into access_token_header 2017-06-23 15:16:58 +02:00
Krombel 9b24e66441 Merge branch 'develop' into access_token_header 2017-06-23 15:16:41 +02:00
Richard van der Hoff cc16cb9281 Merge pull request #475 from matrix-org/rav/fallback_to_memorystore
Fall back to MemoryCryptoStore if indexeddb fails
2017-06-22 16:05:30 +01:00
Richard van der Hoff 45fe4846f2 Fall back to MemoryCryptoStore if indexeddb fails
If we get an error when connecting to th indexeddb, fall back to a
MemoryCryptoStore.

This takes a bit of reorganising, because we don't get the error until we try
to connect to the database.
2017-06-22 15:22:55 +01:00
David Baker 3ca2779d9c Merge pull request #474 from matrix-org/rav/fix_braindead_firefox
Fix load failure in firefox when indexedDB is disabled
2017-06-22 15:22:21 +01:00
Richard van der Hoff 967341b127 fix build error
browser-index isn't transpiled, so can't use var there.
2017-06-22 15:16:23 +01:00
Richard van der Hoff 4e7f9fb805 Fix load failure in firefox when indexedDB is disabled 2017-06-22 15:05:02 +01:00
David Baker f3eb661aad Merge branch 'master' into develop 2017-06-22 11:51:24 +01:00
David Baker 1abf8e23a4 v0.7.13 2017-06-22 11:48:30 +01:00
David Baker 9f1f476f43 Prepare changelog for v0.7.13 2017-06-22 11:48:29 +01:00
David Baker 1a9d61c92a Merge pull request #473 from matrix-org/rav/no_require_indexeddb
Fix failure on Tor browser
2017-06-22 11:22:36 +01:00
David Baker 6ad465e3c0 Merge pull request #472 from matrix-org/rav/indexeddb_fixes
Fix issues with firefox private browsing
2017-06-22 11:15:03 +01:00
Richard van der Hoff 8ef947722f Fail gracefully on browsers without indexeddb
If we don't have indexeddb at all, don't try to make an indexeddb crypto store.
2017-06-22 07:49:28 +01:00
Richard van der Hoff 6e6b5c95a3 indexeddb worker: make clearDatabase work without having connected
... so that we can clear the database during login from a temporary client.
2017-06-21 21:13:41 +01:00
Richard van der Hoff fa593a7a37 Treat errors when deleting indexeddb as non-fatal
If we get an error when vaping the indexeddb, carry on regardless
2017-06-21 18:06:21 +01:00
Richard van der Hoff 7fcccad0ae Fix another round of test failures
'blocked' is *not* a fatal situation when opening or deleting databases.
2017-06-21 11:26:02 +01:00
David Baker e8ce94ade2 Merge pull request #471 from matrix-org/rav/fix_test_race
Fix a race in a test
2017-06-21 09:28:16 +01:00
Richard van der Hoff 6055f038ee Fix a race in a test
startClient was written in such a way that it would leave a flush() running,
which could sometimes interfere with the rest of the test (or even subsequent
tests), causing sporadic test failures. Make sure that the flush completes
before we move on.

Fix a test which turned out to be relying on that behaviour (there was a flush
which ended up being a no-op, thus effectively inserting a pause allowing the
sync promise to complete.

Fix a beforeEach handler which was relying on startClient resolving to
undefined.
2017-06-21 07:57:38 +01:00
Richard van der Hoff 6a1f40eeab Make sure we shut down the crypto module properly
listening to the sync STOPPED event doesn't cut it, because the app might (and
does, in the case of react-sdk) do a removeAllListeners.
2017-06-20 23:51:25 +01:00
Richard van der Hoff ca01589e50 Fix another round of test failures
'blocked' is *not* a fatal situation when opening or deleting databases.
2017-06-20 17:36:35 +01:00
David Baker cca891644d Merge pull request #470 from matrix-org/rav/fix_error_on_shutdown
Avoid throwing an unhandled error when the indexeddb is deleted
2017-06-20 15:46:55 +01:00
Richard van der Hoff cd19578d80 Avoid throwing an unhandled error when the indexeddb is deleted
Hopefully this will fix the vector-web test failures (the
OutgoingRoomRequestManager throws an exception because the indexeddb is being
deleted just as it's getting started).
2017-06-20 15:36:05 +01:00
Richard van der Hoff c96f7e5a13 Merge pull request #469 from matrix-org/rav/fix_jsdoc_build
fix jsdoc
2017-06-20 14:01:14 +01:00
Richard van der Hoff d7f92b4f72 fix jsdoc 2017-06-20 13:51:08 +01:00
Richard van der Hoff 70a5208fcc Run gendoc as part of the travis build
... so that I don't get surprised by it not working when it lands on develop
2017-06-20 13:33:04 +01:00
Richard van der Hoff 8c9150db66 Merge pull request #468 from matrix-org/rav/handle_forwarded_room_key_2
Handle m.forwarded_room_key events
2017-06-20 13:18:44 +01:00
Richard van der Hoff 1f86dbd12f Add support for forwarding room keys to megolm
when we receive a m.forwarded_room_key, add it to the crypto store, but
remember who forwarded it to us, so we can decide whether to trust them
separately.
2017-06-20 12:39:36 +01:00
Richard van der Hoff cfa871c076 event.js: Add support for forwardingCurve25519KeyChain 2017-06-20 11:51:30 +01:00
Richard van der Hoff f355661522 fix a lint error 2017-06-20 11:51:30 +01:00
Richard van der Hoff be3fb0f917 Make a start on a unit test for megolm alg impl
not much here yet, but it's a start at least.
2017-06-20 11:51:30 +01:00
Richard van der Hoff e2f4c0ffd1 Rename megolm integration tests
I'm going to introduce some separate unit tests, so let's give this a different
filename to reduce confusion.
2017-06-20 11:51:11 +01:00
Richard van der Hoff 210a53a3a5 Refactor internal OlmDevice methods
Rearrange the way _getInboundGroupSession and _saveInboundGroupSession work, so
that we can add more things to the storage without growing the parameter list
forever.
2017-06-20 11:51:11 +01:00
Richard van der Hoff 5049919855 Replace keysProved and keysClaimed
These terms were somewhat confusing (and, in the case of megolm, misleading),
so replace them with explicit senderCurve25519Key and claimedEd25519Key fields.
2017-06-20 11:51:11 +01:00
Richard van der Hoff ce187786cb Merge remote-tracking branch 'origin/develop' into room_key_sharing 2017-06-20 11:28:53 +01:00
Richard van der Hoff e6b35a9237 Run the crypto tests under travis (#467)
The crypto tests haven't been running since things got rearranged to expect
Olm in a global (41864d4). Reinstate them.
2017-06-20 10:44:03 +01:00
Richard van der Hoff 82e5e9cf4a Merge branch 'develop' into room_key_sharing 2017-06-19 17:38:35 +01:00
David Baker d7e1910076 Merge pull request #466 from matrix-org/rav/improve_indexeddb_errors
Improve error reporting from indexeddbstore.clearDatabase
2017-06-19 16:00:38 +01:00
Richard van der Hoff 009c28ae50 Improve error reporting from indexeddbstore.clearDatabase
- to help understand when it gets stuck in tests
2017-06-19 15:51:55 +01:00
David Baker db66023102 v0.7.12 2017-06-19 11:58:56 +01:00
David Baker 4d8dc1a0c4 Prepare changelog for v0.7.12 2017-06-19 11:58:55 +01:00
Krombel e0a5edeb04 implement usage of Authorization-Header instead of query-param for access_token 2017-06-16 12:33:42 +02:00
David Baker ffd9a01e2f v0.7.12-rc.1 2017-06-15 17:13:26 +01:00
David Baker 25a8c79951 Prepare changelog for v0.7.12-rc.1 2017-06-15 17:13:25 +01:00
Matthew Hodgson c8674ff104 Merge pull request #462 from t3chguy/t3chguy/voip/force_turn
allow setting iceTransportPolicy to relay through forceTURN option
2017-06-12 21:43:50 +01:00
Michael Telatynski a40b10f53c allow setting iceTransportPolicy to relay through forceTURN option
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-06-12 20:23:57 +01:00
David Baker 79fa944402 v0.7.11 2017-06-12 15:17:53 +01:00
David Baker ed3cdeec74 Prepare changelog for v0.7.11 2017-06-12 15:17:53 +01:00
David Baker 05d50d457c Merge remote-tracking branch 'origin/develop' into release-v0.7.11 2017-06-12 15:16:18 +01:00
David Baker 2531db84a6 Merge pull request #460 from matrix-org/rav/send_message_logging
Add a bunch of logging around sending messages
2017-06-12 13:37:50 +01:00
Richard van der Hoff 96c1126fe5 Add a bunch of logging around sending messages
In an attempt to diagnose https://github.com/vector-im/riot-web/issues/4278,
add some debug to make the rageshakes more useful.
2017-06-12 13:32:10 +01:00
David Baker bb5038b8b2 v0.7.11-rc.1 2017-06-09 20:23:30 +01:00
David Baker 0c65162349 Prepare changelog for v0.7.11-rc.1 2017-06-09 20:23:29 +01:00
David Baker 17cc12844d Merge pull request #458 from matrix-org/rav/resolve_timeline_window_quickly
Make TimelineWindow.load resolve quicker if we have the events
2017-06-09 20:06:28 +01:00
Richard van der Hoff 6cfcf92a28 Make TimelineWindow.load resolve quicker if we have the events
If we have the events in memory, let TimelineWindow.load() return
a resolved promise, so that the UI can show the view straight away instead
of showing the spinner.
2017-06-09 14:59:11 +01:00
Luke Barnard 6ed9a85dca Add API for POST /user_directory/search (#457)
* Add API for POST /user_directory/search

This takes a JSON body of the form:
```json
{
    "term": "search term",
    "limit": 42,
}
```
where "term" is the term to match against user IDs, display names and domains and "limit" is the maximum number of results to return (which is defaulted server-side).

The response body looks like
```json
{
    "results ": [
        { "user_id": "@someid:mydomain.com", "display_name": "Some Name", "avatar_url": "mx://..." },
        ...
    ],
    "limited": false
}
```
where "limited" indicates whether the "limit" was used to truncate the list.
2017-06-07 15:34:07 +01:00
Richard van der Hoff 0371265fea Send a cancellation for room key requests (#456)
* Send a cancellation for room key requests

When we receive a room key, cancel any pending requests we have open for that
key.
2017-06-07 14:00:47 +01:00
Richard van der Hoff de257b34c0 Merge pull request #454 from matrix-org/rav/key_share/incoming
Implement sharing of megolm keys
2017-06-07 13:17:43 +01:00
Richard van der Hoff 4b6575d94a Fix jsdocs 2017-06-07 11:02:27 +01:00
Richard van der Hoff 2c54d76085 Implement sharing of megolm keys 2017-06-06 14:46:54 +01:00
Richard van der Hoff 70f39ed760 Fix lint failure 2017-06-06 14:46:24 +01:00
Richard van der Hoff 1c6652483b Merge pull request #449 from matrix-org/rav/handle_room_key_requests
Process received room key requests
2017-06-06 14:30:56 +01:00
Richard van der Hoff ab7e0a9266 Merge branch 'room_key_sharing' into rav/handle_room_key_requests 2017-06-06 14:30:34 +01:00
Richard van der Hoff ff323d00af Merge pull request #448 from matrix-org/rav/send_room_key_requests
Send m.room_key_request events when we fail to decrypt an event
2017-06-06 14:25:40 +01:00
Richard van der Hoff ea2a04135f Send a room key request on decryption failure
When we are missing the keys to decrypt an event, send out a request for those
keys to our other devices and to the original sender.
2017-06-06 14:24:19 +01:00
Richard van der Hoff 6d88c76464 Storage layer for management of outgoing room key requests 2017-06-06 14:24:19 +01:00
Luke Barnard 9b188ca87d Use single room object for duration of peek (#453)
Use single room object for duration of peek

Instead of getting the room by ID every time the room is polled for events, which could cause issues if the state of the room is modified from under the peeking logic (if the user joined the room or registered etc.)
2017-06-06 12:15:30 +01:00
Richard van der Hoff 1664312c80 Address review comments
Avoid gut-wrenching properties on IncomingRoomKeyRequest.
2017-06-05 16:07:38 +01:00
David Baker 38baa42ebb Merge pull request #451 from matrix-org/dbkr/stop_peeking
Stop peeking when a matrix client is stopped
2017-06-05 14:16:35 +01:00
David Baker 654322e896 Stop peeking when a matrix client is stopped
Otherwise we get very confused when the peek poll returns after
the client is stopped.
2017-06-05 14:04:41 +01:00
Richard van der Hoff 3f70f532b7 Update README.md
lack of olm is a warning, not an exception
2017-06-05 09:24:59 +01:00
Richard van der Hoff 6ba214a259 Merge pull request #450 from arxcode/develop
Update README: Clarify how to install libolm
2017-06-05 09:22:34 +01:00
arxcode caf73f387f Update README: Clarify how to install libolm 2017-06-04 23:24:11 +02:00
Matthew Hodgson 9a81ca9fab v0.7.10 2017-06-02 01:02:01 +01:00
Matthew Hodgson 0edf19a871 Prepare changelog for v0.7.10 2017-06-02 01:02:01 +01:00
Matthew Hodgson 6989f6c835 switch to using new media constraints to allow device selection to work 2017-06-01 21:57:58 +01:00
Richard van der Hoff 2daa39520a Room key request cancellation handling 2017-06-01 18:30:32 +01:00
Richard van der Hoff c8eca50f43 Processing of received room key requests
Doesn't actually do any of the crypto magic yet.
2017-06-01 18:30:26 +01:00
Richard van der Hoff de844f1a32 Merge pull request #447 from matrix-org/rav/fix_indexeddb_deletion
indexeddb-crypto-store: fix db deletion
2017-06-01 17:34:37 +01:00
Richard van der Hoff 97951e1c1a Merge pull request #446 from matrix-org/rav/load_olm_from_global
Load Olm from the global rather than requiring it.
2017-06-01 15:41:22 +01:00
Richard van der Hoff 2edbed8528 indexeddb-crypto-store: fix db deletion
Add an `onversionchange` listener to close the db, so that we can delete it
without blocking.
2017-06-01 15:37:27 +01:00
Richard van der Hoff 24937910c7 Merge remote-tracking branch 'origin/develop' into rav/load_olm_from_global 2017-06-01 15:31:27 +01:00
Richard van der Hoff 5cd441fb48 Add a warning to the changelog 2017-06-01 15:30:00 +01:00
Richard van der Hoff 06b956bd75 disable e2e test when there is no e2e 2017-06-01 13:16:10 +01:00
Richard van der Hoff 41864d46c3 Load Olm from the global rather than requiring it.
This means that we can avoid confusing everybody in the world about how to
webpack js-sdk apps.
2017-06-01 13:09:48 +01:00
Matthew Hodgson f6622e0bcd unbreak riot-web release process 2017-06-01 02:41:47 +01:00
Matthew Hodgson 0f30d21fa2 v0.7.9 2017-06-01 01:41:06 +01:00
Matthew Hodgson 4257c8c9f5 Prepare changelog for v0.7.9 2017-06-01 01:41:06 +01:00
Richard van der Hoff 331859d383 Merge pull request #445 from matrix-org/rav/indexeddb_crypto_store
Initial framework for indexeddb-backed crypto store
2017-05-31 18:06:45 +01:00
Richard van der Hoff ef03b708a8 Add MatrixClient.clearStores
- to clear both sets of storage on logout
2017-05-31 17:22:07 +01:00
Richard van der Hoff 716d098361 Address Kegan's review comments
jsdoc mostly.
2017-05-31 16:05:00 +01:00
Richard van der Hoff d887057660 Merge pull request #444 from matrix-org/rav/factor_out_reemit
Factor out reEmit to a common module
2017-05-31 14:22:34 +01:00
Richard van der Hoff 7efbfebb4d Factor out reEmit to a common module
and rewrite it to use modern JS while we're at it
2017-05-31 11:01:48 +01:00
Richard van der Hoff 4c7afe5af0 Initial framework for indexeddb-backed crypto store
Doesn't do anything useful yet - just demonstrates a framework for how I hope
it will fit into the sdk.
2017-05-30 23:25:07 +01:00
Richard van der Hoff 676515cf27 Merge pull request #443 from matrix-org/rav/es6ify_algorithm_base
crypto/algorithms/base.js: Convert to es6
2017-05-23 16:36:42 +01:00
Richard van der Hoff 0eb5b0fdfa Merge pull request #435 from t3chguy/maySendRedactionForEvent
maySendRedactionForEvent for userId
2017-05-23 15:45:46 +01:00
Richard van der Hoff 2feba4787f Merge pull request #441 from matrix-org/rav/get_userid
MatrixClient: add getUserId()
2017-05-23 15:44:34 +01:00
Michael Telatynski 516dc1043e prevent powerLevels being undef
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-23 14:53:11 +01:00
Richard van der Hoff b26c1c57dc crypto/algorithms/base.js: Convert to es6
Convert base to an es6 module with es6 classes, for clarity and to help with
jsdoccing.

Complications are:

* jsdoc gets confused by `export class`, so the exports are separated.

* turns out that extending Error is a bit difficult, so instanceof doesn't work
  on derived Error classes. This only really affects us in one place (app-side
  code shouldn't be doing instanceofs anyway), so just use `name` instead.
2017-05-23 14:32:13 +01:00
Richard van der Hoff 0945ba9e90 Merge pull request #442 from matrix-org/rav/custom_babel_for_jsdoc
Run jsdoc on a custom babeling of the source
2017-05-23 14:28:41 +01:00
Michael Telatynski 69ed6f283d fix based on rich's feedback
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-23 12:55:05 +01:00
Richard van der Hoff 9eef850d0c Run jsdoc on a custom babeling of the source
jsdoc can't read our raw source, because of our dangling commas in function
calls. On the other hand, running on /lib means that a lot of the useful
information about exports is lost and you end up having to jump through hoops
to get jsdoc to generate the right thing.

This uses a separate run of babel (with all the presets turned off) to generate
source which is almost identical to the input, but lacks trailing commas.

(https://babeljs.io/blog/2015/10/31/setting-up-babel-6 says 'Babel 6 ships
without any default transforms, so when you run Babel on a file it will just
print it back out to you without changing anything.' - however, that is,
empirically, not entirely true.)
2017-05-23 12:26:17 +01:00
Richard van der Hoff cf1574d690 MatrixClient: add getUserId()
... I'm amazed we got this far without it.
2017-05-23 10:37:26 +01:00
David Baker d6913e41a0 Merge branch 'master' into develop 2017-05-22 11:33:20 +01:00
David Baker 3c81c295c7 v0.7.8 2017-05-22 11:31:48 +01:00
David Baker 56dfa0c755 Prepare changelog for v0.7.8 2017-05-22 11:31:47 +01:00
Richard van der Hoff 43989be768 Merge pull request #439 from kscz/add_getstoreddeviceforuser
Add in a public api getStoredDevice allowing clients to get a specific device
2017-05-22 09:36:38 +01:00
Kit Sczudlo 822380ac38 Add in a public api getStoredDevice allowing clients to get a specific device
Signed-off-by: Kit Sczudlo <kit@kitscz.com>
2017-05-21 00:30:40 -07:00
David Baker 8c37d9ac9a v0.7.8-rc.1 2017-05-19 10:34:31 +01:00
David Baker e40b8461f7 Prepare changelog for v0.7.8-rc.1 2017-05-19 10:34:30 +01:00
David Baker a3f45b466a Merge pull request #438 from matrix-org/rav/release_signing
Attempt to rework the release-tarball-signing stuff
2017-05-19 10:03:36 +01:00
Richard van der Hoff 672ad68c64 release.sh: download the tarball from git to verify it 2017-05-18 18:58:50 +01:00
David Baker 4ccec13739 Fix build: move uglifyjs dep to uglify-js
uglifyjs have gained a hyphen for some reason, and replaced th
old one with a stub package.
2017-05-17 11:21:20 +01:00
Michael Telatynski 09529a1aa8 lets please the ESLint gods
`--max-warnings 115` :')

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-16 14:16:42 +01:00
Michael Telatynski d182fd6bb7 can't redact queued/not_sent
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-16 14:12:29 +01:00
Michael Telatynski 36bf123e2b maySendRedactionForEvent for userId
done using a private helper so kick/ban etc perms can be done
easily at a later stage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-16 14:04:55 +01:00
Matthew Hodgson 92cfbf655f Merge pull request #427 from t3chguy/electron_media_select
ability to specify webrtc audio/video inputs for the lib to request
2017-05-15 02:10:01 +01:00
Matthew Hodgson fbef701179 Merge pull request #434 from t3chguy/t3chguy/screen_share_firefox
make screen sharing call FF friendly :D
2017-05-15 00:16:33 +01:00
Michael Telatynski 0415b9cf4c make screen sharing call FF friendly :D
FF is uber nice that it lets us select the display
does not seem to allow the composite ALL displays though

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-05-14 23:48:26 +01:00
Luke Barnard cb9a9e8d50 Implement API for username availability (#432)
Requires synapse with https://github.com/matrix-org/synapse/pull/2183, https://github.com/matrix-org/synapse/pull/2209 and https://github.com/matrix-org/synapse/pull/2213
2017-05-11 09:14:45 +01:00
Richard van der Hoff 6021c1c6b1 Merge pull request #431 from matrix-org/rav/fix_device_list_yet_again
Fix race in device list updates
2017-05-05 13:29:53 +01:00
Richard van der Hoff 655be2fa2e Fix race in device list updates
Don't consider device lists up-to-date when we have another request for the
relevant user in the queue.

Fixes https://github.com/vector-im/riot-web/issues/3796.
2017-05-05 12:34:00 +01:00
Michael Telatynski 98491a63a7 ability to specify webrtc audio/video inputs for the lib to request 2017-04-27 16:06:34 +01:00
David Baker acd7f15c83 Merge pull request #424 from matrix-org/rob/nocam
WebRTC: Support recvonly for video for those without a webcam
2017-04-26 18:23:52 +01:00
Richard van der Hoff 5020d4e99f Rework device list tracking logic (#425)
Yet another attempt at fixing
https://github.com/vector-im/riot-web/issues/2305.

This now implements the algorithm described at
http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#tracking-the-device-list-for-a-user:

* We now keep a flag to tell us which users' device lists we are tracking. That
  makes it much easier to figure out whether we should care about device-update
  notifications from /sync (thereby fixing
  https://github.com/vector-im/riot-web/issues/3588).

* We use the same flag to indicate whether the device list for a particular
  user is out of date. Previously we did this implicitly by only updating the
  stored sync token when the list had been updated, but that was somewhat
  complicated, and in any case didn't help in cases where we initiated the key
  download due to a user joining an encrypted room.

Also fixes https://github.com/vector-im/riot-web/issues/3310.
2017-04-25 17:56:01 +01:00
David Baker 9693c30209 Merge branch 'master' into develop 2017-04-25 10:51:08 +01:00
David Baker 2b6f8adc64 v0.7.7 2017-04-25 10:49:28 +01:00
David Baker 822f5927e5 Prepare changelog for v0.7.7 2017-04-25 10:49:26 +01:00
David Baker 0f6e9d7b9d v0.7.7-rc.1 2017-04-21 18:15:49 +01:00
David Baker 99f3e3f09e Prepare changelog for v0.7.7-rc.1 2017-04-21 18:15:48 +01:00
David Baker aa81c96a98 Automatically complete dummy auth
Dummy auth flows, bu definition, do not require a response from
the user, and so should just be completed automatically by
interactive-auth.
2017-04-21 18:06:57 +01:00
Richard van der Hoff 9d532b6c72 Merge pull request #422 from t3chguy/develop
Update istanbul to remove minimatch DoS Warning
2017-04-21 12:12:32 +01:00
Luke Barnard 4c63906b8f Implement API for setting RM (#419)
* Implement API for setting RM

This is now stored on the server with similar treatment to RRs. The server will only store the specified eventId as the current read marker for a room if the event is ahead in the stream when compared to the existing RM. The exception is when the RM has never been set for this room for this user, in which case the event ID will be stored as the RM without any comparison.

This API also allows for an optional RR event ID to be sent in the same request. This is because it might be the common case for some clients to update the RM at the same time as updating the RR.

See design: https://docs.google.com/document/d/1UWqdS-e1sdwkLDUY0wA4gZyIkRp-ekjsLZ8k6g_Zvso/edit

See server-side PRs: https://github.com/matrix-org/synapse/pull/2120, https://github.com/matrix-org/synapse/pull/2128
2017-04-20 09:43:33 +01:00
Robert Swain dd2a870227 webrtc/call: Unmute remote audio element when setting 2017-04-20 06:41:29 +02:00
Robert Swain 88948c3cfd webrtc/call: Always offer to receive audio/video for video call
This allows people without (or denying access to) a webcam to make a
video call and receive audio and video from the peer.
2017-04-20 06:35:03 +02:00
Robert Swain b33dcfe6ff webrtc/call: Fall back to recvonly if camera/mic access is denied
Users of MatrixCall will need to present some sensible UX for this.
2017-04-20 06:32:52 +02:00
Robert Swain 2c15bdae04 Merge pull request #423 from matrix-org/rob/more-distinct-callid
webrtc/call: Make it much less likely that callIds collide locally
2017-04-19 17:34:15 +02:00
Robert Swain 2f45633312 webrtc/call: Make it much less likely that callIds collide locally
Previously if two calls were constructed within 1ms they could have the
same id.
2017-04-19 16:51:23 +02:00
Michael Telatynski fdd42fbc6d Update dependencies to remove minimatch DoS Warning
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-04-19 12:50:21 +01:00
Richard van der Hoff 54a6f5d425 Merge pull request #420 from matrix-org/dbkr/fix_dummy_auth
Automatically complete dummy auth
2017-04-13 14:23:33 +01:00
David Baker 68d9662fe5 Automatically complete dummy auth
Dummy auth flows, bu definition, do not require a response from
the user, and so should just be completed automatically by
interactive-auth.
2017-04-12 18:36:23 +01:00
David Baker 4f0987da01 Merge branch 'master' into develop 2017-04-12 09:58:26 +01:00
David Baker 625697e097 v0.7.6 2017-04-12 09:56:46 +01:00
David Baker 92b14f20d2 Prepare changelog for v0.7.6 2017-04-12 09:56:45 +01:00
Richard van der Hoff dd069647d1 Merge pull request #418 from matrix-org/dbkr/release_script_dont_leave_me_in_gh_pages
Don't leave the gh-pages branch checked out
2017-04-10 16:36:25 +01:00
David Baker 4523ae7d29 Checkout release branch *before* exiting script 2017-04-10 16:15:06 +01:00
David Baker 19e5eda773 Don't leave the gh-pages branch checked out
After a pre-release, check out the release branch again rather
than leaving the working copy on the gh-pages branch
2017-04-10 15:52:37 +01:00
David Baker 070d58ac0e v0.7.6-rc.2 2017-04-10 14:52:10 +01:00
David Baker 2d70f69857 Prepare changelog for v0.7.6-rc.2 2017-04-10 14:52:09 +01:00
David Baker 7a3acfa6a7 Merge remote-tracking branch 'origin/develop' into release-v0.7.6 2017-04-10 14:50:16 +01:00
David Baker 8ac0d12d1e Merge pull request #416 from matrix-org/dbkr/feature_detect_webworker
Add feature detection for webworkers
2017-04-10 11:28:40 +01:00
David Baker 86164103f0 Allow webworker API to be passed in
So it can be used from Node with one of the compatible APIs
2017-04-10 10:02:06 +01:00
David Baker b9c71ef03f Add feature detection for webworkers
Only use web worker store if we have web workers available
2017-04-07 17:45:45 +01:00
David Baker 7ffff761d5 Merge remote-tracking branch 'origin/develop' into release-v0.7.6 2017-04-07 17:01:25 +01:00
Richard van der Hoff 7d4366473d Merge pull request #415 from matrix-org/dbkr/fix_release_script
Fix release script
2017-04-07 17:00:12 +01:00
David Baker e63c660162 Fix release script
Publish to npm before switching to the doc branch: previously we
published from master, but since we now now longer merge
pre-releases to master, publish from the release branch (just
not the doc branch because that won't work).
2017-04-07 16:55:38 +01:00
David Baker 1762f9d68e v0.7.6-rc.1 2017-04-07 16:44:21 +01:00
David Baker 76287eed2c Prepare changelog for v0.7.6-rc.1 2017-04-07 16:44:20 +01:00
David Baker 5a764bbaa2 Merge pull request #414 from matrix-org/dbkr/indexeddb_save_after_first_sync
Make indexeddb save after the first sync
2017-04-07 16:26:03 +01:00
David Baker ce9e69c9e0 Merge remote-tracking branch 'origin/develop' into dbkr/indexeddb_save_after_first_sync 2017-04-07 16:21:54 +01:00
David Baker 3a74e1f154 Merge remote-tracking branch 'origin/dbkr/indexeddb_webworker_dont_transfer_sync' into dbkr/indexeddb_save_after_first_sync 2017-04-07 16:21:21 +01:00
David Baker 6df4a36da9 Merge pull request #413 from matrix-org/dbkr/indexeddb_webworker_dont_transfer_sync
Make indexeddb startup faster
2017-04-07 16:20:44 +01:00
David Baker 4e38b51958 Merge remote-tracking branch 'origin/develop' into dbkr/indexeddb_webworker_dont_transfer_sync 2017-04-07 15:12:54 +01:00
David Baker b6c036af25 Merge pull request #412 from matrix-org/dbkr/indexeddb_webworker
Add ability to do indexeddb sync work in webworker
2017-04-07 15:12:22 +01:00
David Baker dd789a8dcc Merge branch 'dbkr/indexeddb_webworker_dont_transfer_sync' into dbkr/indexeddb_save_after_first_sync 2017-04-07 15:10:17 +01:00
David Baker ca83b858c0 lint 2017-04-07 15:09:42 +01:00
David Baker 0f29952a1c Smush connect() and init() together 2017-04-07 15:06:38 +01:00
David Baker e2d7b465ae Merge branch 'dbkr/indexeddb_webworker' into dbkr/indexeddb_webworker_dont_transfer_sync 2017-04-07 15:01:42 +01:00
David Baker 8985dc2f7e Update import in example 2017-04-07 14:42:26 +01:00
David Baker cf1731792c Add separate import file for indxexeddb worker
And removing it from the main one
2017-04-07 14:41:12 +01:00
David Baker 4c200cdd49 lint 2017-04-07 14:26:43 +01:00
David Baker 5c8eacddde Remove old comment 2017-04-07 12:01:31 +01:00
David Baker 2668177210 Oops, moved the variable 2017-04-07 11:58:16 +01:00
David Baker 62be08f063 Make sure worker setup doesn't race 2017-04-07 11:55:13 +01:00
David Baker ab2a67a012 Don't try to send Error objects 2017-04-07 11:42:18 +01:00
David Baker 3ceeee7298 Typos 2017-04-07 11:40:21 +01:00
David Baker 2d7576f29b Doc usage of the webworker class 2017-04-07 11:37:31 +01:00
David Baker 6e25a17afb Typos 2017-04-07 11:26:52 +01:00
David Baker 0715682a8b Name IndexedDBStoreWorker consistently 2017-04-07 11:22:07 +01:00
David Baker cfff30c314 Use once to clean up listener 2017-04-07 11:10:57 +01:00
David Baker b53318ecb7 Make indexeddb save after the first sync
Save the sync data to indexeddb after the first catch-up

Fixes https://github.com/vector-im/riot-web/issues/3527
2017-04-06 18:48:54 +01:00
David Baker 039a3e258b Make indexeddb startup faster
Don't needlessly transfer data from the worker to the main script
only to immediately transfer it right back again.
2017-04-06 18:09:12 +01:00
David Baker 18806e5524 lint 2017-04-06 16:58:13 +01:00
David Baker 0594a8d03a Don't set the sync token when loading indexeddb
Setting the sync token here marks the memory store part as being
synced up to that point, but it isn't because the sync data hasn't
yet been injected into it.

The sync token will be set in the normal way when the cached sync
response is processed, at which point it will be accurate because
the cached sync data will actually have been processed by the sync
code and saved to the memory store.
2017-04-06 16:41:49 +01:00
David Baker 5a575d61b6 Comment really inefficient bit 2017-04-06 16:33:52 +01:00
David Baker 42c3cf2545 Use correct variable name
From https://github.com/matrix-org/matrix-js-sdk/pull/401
2017-04-06 16:09:28 +01:00
David Baker bf6490739d Fix tests
Make setSyncData return promises in a few places and fix all the
places the tests assume that /sync being flushed == the sync
result being processed.
2017-04-06 15:20:50 +01:00
David Baker 9815c0a866 Remove debug exception catching 2017-04-06 11:47:55 +01:00
David Baker f5f05a9a91 Add ability to do indexeddb sync work in webworker 2017-04-06 11:09:11 +01:00
David Baker b392656d60 Merge pull request #409 from matrix-org/dbkr/indexeddb_worker_localsplit
Move more functionality to the indexeddb backend
2017-04-06 10:58:37 +01:00
David Baker 6737d091fd Merge pull request #410 from matrix-org/luke/fix-indicate-error-when-reconnecting
Indicate syncState ERROR after many failed /syncs
2017-04-05 16:07:53 +01:00
Luke Barnard 9f924c3510 Respond to code review
- failedSyncCount -> this._failedSyncCount
- Reset at a point which is more obviously after a successful sync, and add comments to clarify the recursive syncage.
- Sync state will now flip from RECONNECTING to ERROR after FAILED_SYNC_ERROR_THRESHOLD syncs
2017-04-05 09:40:06 +01:00
Luke Barnard 2ea66d2e81 Indicate syncState ERROR after many failed /syncs
when a /sync leads to an error, increase a counter and use the counter to decide which state to be in when starting keepAlives. If the count is above a certain threshold (arbitrary 3 chosen here), switch from RECONNECTING to ERROR to show that the server is facing difficulties that aren't affeting is ability to return on /versions.
2017-04-04 18:12:20 +01:00
David Baker d18c238938 Dangling comma 2017-04-04 16:35:39 +01:00
David Baker 8c92e221a3 long line 2017-04-04 16:29:05 +01:00
David Baker bafe9c06d4 Move more functionality to the indexeddb backend
Now the backend drives the sync accumulator as well. Also moves
the backend out to a separate file.
2017-04-04 16:25:58 +01:00
David Baker cad6ec854e Merge pull request #407 from matrix-org/dbkr/indexeddb_refactor_2
Further reorganising of indexeddb sync code
2017-04-04 13:13:18 +01:00
David Baker 47a0398b62 Remove other two lines 2017-04-04 10:43:57 +01:00
David Baker 11f5ae3c20 Merge remote-tracking branch 'origin/dbkr/indexeddb_hide_internals' into dbkr/indexeddb_refactor_2 2017-04-04 10:39:23 +01:00
David Baker 61cf853eb5 Merge pull request #406 from matrix-org/dbkr/indexeddb_hide_internals
Change interface of IndexedDBStore: hide internals
2017-04-04 10:38:43 +01:00
David Baker f72884ac19 Spelling 2017-04-04 10:36:31 +01:00
David Baker b72b38b0a3 Add missed methods to stub/memory store
and fix tests
2017-04-03 17:45:58 +01:00
David Baker 6a2465329a Add jsdoc 2017-04-03 17:12:30 +01:00
David Baker 4cb80588e9 Merge branch 'dbkr/indexeddb_hide_internals' into dbkr/indexeddb_refactor_2 2017-04-03 16:41:02 +01:00
David Baker 753f11e0e9 Stray jsdoc line that didn't get removed 2017-04-03 15:39:08 +01:00
David Baker c0bd2c8945 Further reorganising of indexeddb sync code
* Make sync communicate with the sync accumulator via the store
 * Consequently get rid of getSyncAccumulator as it's now
   unnecessary.
 * Make the bit that gets the saved sync response async, because
   we'll need it to be when it's coming over postMessage from a
   webworker.
2017-03-31 18:18:53 +01:00
David Baker aebbe4f254 Change interface of IndexedDBStore: hide internals
Hide the IndexedDBBackend and SyncAccumulator objects and have
indexeddbstore instaniate them itself, rather than making the app
instantiate them. Pass the approipriate options through. This
gives us much more flexibility to move things around under the
hood.
2017-03-31 14:53:13 +01:00
David Baker 68948dbaeb Merge pull request #405 from matrix-org/dbkr/fix_notifs_on_refresh
Don't be SYNCING until updating from the server
2017-03-28 15:16:01 +01:00
David Baker a38917f920 Don't be SYNCING until updating from the server
Syncing should probably mean the stream is up to date and
streaming messages in real-time from the server, which is not the
case if we've only loaded the cached response. Stay PREPARED until
we actually get the latest from the server.
2017-03-28 14:57:11 +01:00
Kegsay f52e198b17 Merge pull request #403 from matrix-org/kegan/dont-log-store-data
Don't log the entire /sync response
2017-03-24 15:09:14 +00:00
Kegan Dougal dec734346b Don't log the entire /sync response
The console will maintain a strong ref to this object, which may exacerbate
memory leaks.
2017-03-24 14:15:35 +00:00
David Baker a73f10edd4 Merge pull request #402 from matrix-org/rob/webrtc-srcobject
webrtc/call: Assign MediaStream to video element srcObject
2017-03-24 14:14:22 +00:00
Robert Swain 59a7232016 webrtc/call: Wrap long line 2017-03-24 15:00:49 +01:00
Robert Swain d9e6aed9da webrtc/call: Assign MediaStream to video element srcObject
video.src = URL.createObjectURL(stream) is on the way out. Firefox will
complain with errors about not being able to play media of type
"text/html" for example.
2017-03-24 14:53:43 +01:00
David Baker e4f52dd1c7 Merge pull request #400 from matrix-org/dbkr/fix_requests_null_deref
Fix undefined reference in http-api
2017-03-23 15:45:35 +00:00
David Baker 2c1e3416e3 Fix undefined reference in http-api
Put the check for 'onprogress' within the check to see if req is
defined, because sometimes it isn't, apparently.
2017-03-23 15:38:15 +00:00
Richard van der Hoff 62090ef119 Merge pull request #382 from fred-wang/master
Add copyright header to event-timeline.js
2017-03-22 17:31:34 +00:00
Richard van der Hoff 52ef8a635f Merge pull request #397 from williamboman/docs/user-global-account-data-event
client: fix docs for user-scoped account_data events
2017-03-22 17:10:52 +00:00
William Boman bf26ccd0a5 client: fix docs for user-scoped account_data events
Signed-off-by: William Boman <william@redwill.se>
2017-03-22 18:02:24 +01:00
Richard van der Hoff 5a55b98650 Merge remote-tracking branch 'origin/master' into develop 2017-03-22 16:55:09 +00:00
David Baker 547333c946 Merge pull request #399 from matrix-org/rav/contributing
Add a CONTRIBUTING for js-sdk
2017-03-22 16:46:21 +00:00
Kegsay 1ed105cb79 Merge pull request #395 from matrix-org/kegan/memleaks
Fix leaking room state objects on limited sync responses
2017-03-22 16:38:42 +00:00
Richard van der Hoff 2ce2928170 Add a CONTRIBUTING for js-sdk
... inspired by synapse's.
2017-03-22 16:36:58 +00:00
Kegan Dougal 14727d75ac Review comments 2017-03-22 15:13:21 +00:00
Kegan Dougal ccbc0b79b8 Add getter/setter for the callback on the MatrixClient instance rather than a startClient opt for ease of gluing code in 2017-03-22 14:29:59 +00:00
Kegan Dougal 5bee0004b2 Revert test as nothing has changed 2017-03-22 13:51:00 +00:00
Kegan Dougal 86fd42dcb5 linting 2017-03-22 12:01:58 +00:00
Kegan Dougal 1e05e0d6f8 Review comments 2017-03-22 11:56:10 +00:00
David Baker 821e0ed6ce Merge pull request #396 from matrix-org/dbkr/ui_auth_bg_requests
Extend 'ignoreFailure' to be 'background'
2017-03-22 11:56:07 +00:00
David Baker 66ce31f6d6 Add docs. 2017-03-21 18:40:00 +00:00
David Baker cf486aedbd Extend 'ignoreFailure' to be 'background'
This allows us to also use it to decide whether or not to show
the app as busy in the UI. We pass this flag up into the
makeRequest callback so it can use it as such.
2017-03-21 18:37:08 +00:00
David Baker 1b0f22c4ae Merge pull request #388 from matrix-org/dbkr/x_show_msisdn
Add x_show_msisdn parameter to register calls
2017-03-21 13:41:34 +00:00
Kegan Dougal 55acf21aa6 Linting 2017-03-20 12:06:37 +00:00
Kegan Dougal dc8a2670ab Unbreak tests 2017-03-20 12:05:22 +00:00
Kegan Dougal b666ec1f4d Fix memory leak on limited room responses 2017-03-20 11:46:50 +00:00
Kegan Dougal 999fc07683 Explain the memory hack 2017-03-20 11:34:50 +00:00
Kegan Dougal 37a186696a Remove spurious changes 2017-03-20 11:27:59 +00:00
Kegan Dougal 107ef27f69 Remove spurious changes 2017-03-20 11:26:59 +00:00
Kegan Dougal 0c1c10a0e0 WIP memleak fixes (341->295MB) 2017-03-16 18:02:17 +00:00
Richard van der Hoff 89de1f9a01 Merge pull request #394 from matrix-org/luke/redact-keep-sender-ts
Update event redaction to keep sender and origin_server_ts
2017-03-16 16:31:34 +00:00
Luke Barnard 602e91da40 Update event redaction to keep sender and origin_server_ts
Required for fixing https://github.com/matrix-org/matrix-js-sdk/issues/387
2017-03-16 16:29:55 +00:00
Kegsay dec4e67135 Merge pull request #393 from matrix-org/kegan/sync-accumulator-limited
Handle 'limited' timeline responses in the SyncAccumulator
2017-03-16 13:52:41 +00:00
Kegan Dougal c30670000d Handle 'limited' timeline responses in the SyncAccumulator
Fixes vector-im/riot-web#3375
2017-03-16 13:20:27 +00:00
David Baker 9d8e81d79c Merge pull request #391 from matrix-org/dbkr/msisdn_better_error_message
Give a better error message if the HS doesn't support msisdn registeration
2017-03-16 12:44:58 +00:00
David Baker 421a35c201 Stray brace 2017-03-16 12:33:24 +00:00
David Baker fcfc7b6cec Better doc & throw consistently 2017-03-16 12:02:06 +00:00
Luke Barnard 2f5da3851b Use XHR onprogress to debounce http._request timeout (#392)
Instead of just using a timeout to reject ongoing requests, reset the timeout when progress is observed (at least when requests are done from browsers).

This is to fix https://github.com/vector-im/riot-web/issues/2737
2017-03-16 09:50:24 +00:00
David Baker 6c2e8eba1c Do no-auth-flow error handling more properly 2017-03-15 16:42:57 +00:00
David Baker c9c3937f4b Move exception throw into _chooseFlow 2017-03-15 14:33:31 +00:00
David Baker 7777cbf6da Lint 2017-03-15 14:21:34 +00:00
David Baker a8a7d327ff Give a better error message
if the HS doesn't support msisdn registeration
2017-03-15 14:14:04 +00:00
David Baker 571fcbe98d Merge remote-tracking branch 'origin/develop' into dbkr/x_show_msisdn 2017-03-15 11:28:24 +00:00
David Baker 8b4b0e0d39 Save the completed flows (#389)
Otherwise we get very confused and go back to the start when given
a response with no flows etc.

Only copy data if none of the 3 fields are defined, since that's
more the situation we actually want to handle.
2017-03-15 11:15:35 +00:00
David Baker c8868a393b Add x_show_msisdn parameter to rregister calls
Request the msisdn stages in the rtegister UI auth, as commented
2017-03-13 17:51:59 +00:00
Matthew Hodgson 49be37dcf9 remove incredibly verbose start/complete sync logging 2017-03-11 15:06:29 +00:00
David Baker 2cd5fe2fec Support msisdn registration and signin (#384)
* Functionality for msisdn signin

 * Add methods to request tokens from synapse to do msisdn
   verification
 * Extend interactive-auth to work with m.email.identity (which
   is significant since email auth is quite a chunk more complex).

* Oops, fix merge

* Fix lint

* Add submitMsisdnToken

* Support the bind_msisdn param to register

Change the bind_email flag to an object with keys 'email' and
'msisdn', backwards compatibly.
2017-03-09 10:56:50 +00:00
David Baker b52a674c1a Merge pull request #383 from matrix-org/dbkr/fix_teamserver
Add getEmailSid
2017-03-06 17:57:06 +00:00
David Baker 8f790d406f Fix lint (add jsdoc) 2017-03-06 09:27:29 +00:00
David Baker 7e2a256229 Add getEmailSid
Other places sometimes need to re-use the email sid to send proof
of ownership of a email address to somewhere else.
2017-03-03 17:40:26 +00:00
David Baker c7a0a560d8 Merge pull request #380 from matrix-org/dbkr/register_ui_auth
Add m.login.email.identity support to UI auth
2017-03-03 13:37:35 +00:00
David Baker 1d591034ff Doc sid param 2017-03-03 10:15:09 +00:00
Frédéric Wang f5ceaffc5c Add copyright header to event-timeline.js 2017-03-02 21:27:51 +01:00
David Baker 3c246a97e8 More docs 2017-03-02 14:15:43 +00:00
David Baker ca395541b6 Backwards compat 2017-03-02 14:09:08 +00:00
David Baker 30d9dec438 Doc error objects pass to stage update 2017-03-02 14:07:25 +00:00
David Baker ff3606478c Merge pull request #381 from matrix-org/rob/maybe-fix-glare
src/client.js: Fix incorrect roomId reference in VoIP glare code
2017-03-02 13:12:29 +00:00
Robert Swain 72caf1886d src/client.js: Fix incorrect roomId reference in VoIP glare code
MatrixCall has a roomId property, but not a room_id property.
2017-03-02 13:36:49 +01:00
David Baker bdae9521cb Copy session ID too 2017-03-01 17:35:18 +00:00
David Baker cb7fb6c7be Keep data from response in any case
Fix up flows if they're absent
2017-03-01 17:10:08 +00:00
David Baker a33e4477af Comment the ignoring of failures 2017-03-01 16:35:37 +00:00
David Baker d435192e22 Remove unnecesary check 2017-03-01 16:33:55 +00:00
David Baker 9480447637 Comment stageState 2017-03-01 16:09:43 +00:00
David Baker 7dbe852606 Comment why we choose flows 2017-03-01 16:07:31 +00:00
David Baker d7216f44f5 PR feedback: Move requestEmailToken...
...to the UI component (in react-sdk)
2017-03-01 16:02:50 +00:00
Luke Barnard fdf09d46af Emit send_event_error when UnknownDeviceError occurs during VoIP operations (#378) 2017-03-01 09:13:57 +00:00
David Baker 32360e7473 Lint 2017-02-28 14:13:50 +00:00
David Baker 90482377b7 Fix linting 2017-02-28 14:08:41 +00:00
David Baker 08a3aea1c7 Fix tests 2017-02-28 13:44:44 +00:00
David Baker 6eebd1e957 Ignore failures when polling for auth completion 2017-02-27 17:23:02 +00:00
David Baker 033bd9bbdc Support polling
for out-of-band auth completion
2017-02-24 17:22:57 +00:00
David Baker af634d3a7d Add m.login.email.identity support to UI auth
Extends the interactive-auth to support m.login.email.identity
This also includes the ability to resume a UI auth session given
the approirpiate information (ie. to resume the auth flow having
click a link in a verification email).
2017-02-24 11:18:21 +00:00
David Baker d42ce3935b Merge pull request #379 from aviraldg/develop
add .editorconfig
2017-02-21 18:18:51 +00:00
Aviral Dasgupta 7a239c81f7 add .editorconfig
Signed-off-by: Aviral Dasgupta <me@aviraldg.com>
2017-02-21 23:40:00 +05:30
David Baker 2b96cd7059 Merge pull request #377 from matrix-org/kegan/indexeddb-account-data
Store account data in the same way as room data
2017-02-21 11:57:35 +00:00
Kegan Dougal 36b8b2c679 Unbreak tests. Add UT for account data 2017-02-21 11:51:45 +00:00
Kegan Dougal 69b3be2419 Store account data in the same way as room data
Previously, we treated the `MatrixEvents` that were in `this.accountData` in
`MatrixInMemoryStore` as the ground truth and saved those to disk and restored
them back upon load. This did not consider that there are **no emitted events**
when they are restored. Riot-Web was listening for a specific account data
event in order to dynamically update the theme. When the page was reloaded, we
dutifully put the right event in `MatrixInMemoryStore`, but failed to emit an
event to tell Riot-Web this. This led to vector-im/riot-web#3247

This patch fixes it by treating the `/sync` response as the ground truth and
ignoring `this.accountData` entirely. This means that upon load, we will be
injecting an `account_data` key into the initial `/sync` response. This will
cause it to be added to `this.accountData` in the store AND cause the event to
be emitted.
2017-02-21 11:39:34 +00:00
Richard van der Hoff bbe74e6987 Merge pull request #376 from matrix-org/rav/delay_otk_generation
Upload one-time keys on /sync rather than a timer
2017-02-21 08:46:59 +00:00
Richard van der Hoff 98d606fca4 Upload one-time keys on /sync rather than a timer
Delay the upload of one-time keys until we have received a sync *without any
to-device messages*. Doing so means that we can try to avoid throwing away our
private keys just before we receive the to-device messages which use them.

Once we've decided to go ahead and upload them, we keep uploading them in
batches of 5 until we get to the desired 50 keys on the server. We then
periodically check that there are still enough on the server.
2017-02-20 16:26:24 +00:00
David Baker 5c4472492a Un-hardcode home directory in jenkins.sh 2017-02-17 19:28:47 +00:00
David Baker a5c726eef9 Merge pull request #374 from matrix-org/kegan/sync-frequency
Increase the WRITE_DELAY on database syncing
2017-02-17 16:54:51 +00:00
Kegan Dougal d23d5b50a3 Increase the WRITE_DELAY on database syncing 2017-02-17 16:49:49 +00:00
David Baker 37e507707d Merge pull request #373 from matrix-org/kegan/delete-all-data-promise
Make deleteAllData() return a Promise
2017-02-17 15:11:09 +00:00
David Baker 44d889b99a Merge pull request #372 from matrix-org/dbkr/room_name_no_banned
Don't include banned users in the room name
2017-02-17 15:10:25 +00:00
Kegan Dougal 491092d040 Make deleteAllDate() return a Promise
It's required for the 'clear cache' button since we only want to refresh the
page after the transaction has gone through.
2017-02-17 15:03:50 +00:00
David Baker b296a1dabe Don't include banned users in the room name 2017-02-17 11:28:28 +00:00
Kegsay 63b8d45ef2 Merge pull request #363 from matrix-org/kegan/indexeddb
Support IndexedDB as a backing store
2017-02-17 11:17:57 +00:00
Kegan Dougal 8220fad855 Informative logging given it won't be spammy 2017-02-17 11:14:02 +00:00
Kegan Dougal 5aa146f0a6 Add function to UTs 2017-02-17 10:41:04 +00:00
Kegan Dougal f8d2661426 Add deleteAllData to the store API 2017-02-17 10:39:30 +00:00
Kegan Dougal 25b8027bd6 Pass in an optional database name to allow for more than one 2017-02-17 10:22:19 +00:00
Kegan Dougal c93c8a79b7 Add startup() to store API 2017-02-17 10:14:12 +00:00
Kegan Dougal 29336e260c Merge branch 'develop' into kegan/indexeddb 2017-02-17 09:41:16 +00:00
Richard van der Hoff 29942e5109 Merge pull request #370 from matrix-org/rav/sync_catchup
Poll /sync with a short timeout while catching up
2017-02-16 16:31:45 +00:00
Richard van der Hoff 926fee8493 fix typo 2017-02-16 16:16:13 +00:00
Kegan Dougal eedaacd256 Merge branch 'develop' into kegan/indexeddb 2017-02-16 16:10:56 +00:00
Kegan Dougal e926aa1bf8 Explain limits on maxTimelineEntries 2017-02-16 15:49:13 +00:00
Richard van der Hoff 597f981fec Poll /sync with a short timeout while catching up
On first connect, or after a disconnection, poll /sync with timeout=0 until
we get no to_device messages back. This will allow us to figure out whether we
have more to_device messages queued up for us on the server, which in turn will
help us fix a bug with clearing out one-time-keys too quickly.
2017-02-16 15:21:24 +00:00
Richard van der Hoff 777fdfbcfa Correct/improve comments about sync states 2017-02-16 14:03:41 +00:00
Kegan Dougal aafb587085 Don't break causality when rolling back state
We need to use the *previous* state when rolling back or else
causality breaks. Consider the messages:
 - m.room.member : Alice
 - Alice: 1
 - Alice: 2
 - m.room.member : Alice -> Bob
 - Bob: 3
 - Bob: 4
 If we roll back 4 messages (to Alice: 2), we want the rolled
 back m.room.member value to be "Alice" and NOT Bob. This
 means we need to look at the previous state of the m.room.member
 event and not the current state.
2017-02-16 12:35:34 +00:00
Kegan Dougal 1eb2576dbe More copyyright headers on touched files 2017-02-16 11:31:33 +00:00
Kegan Dougal 9a9646d012 Review comments 2017-02-16 11:28:51 +00:00
Richard van der Hoff 8ecb05a094 Merge pull request #368 from matrix-org/rav/fix_coverage
Make test coverage work again
2017-02-15 20:40:09 +00:00
Richard van der Hoff 8c8ca0584f fix package.json 2017-02-15 19:37:40 +00:00
Kegan Dougal b02ba08abc Remove debug comments 2017-02-15 16:53:31 +00:00
Richard van der Hoff 75d213f6b3 Make test coverage work again
* don't try to run npm under istanbul
* use _mocha instead of mocha because
  https://github.com/gotwarlost/istanbul/issues/44#issuecomment-16093330

Also:
* write a text report for better travis
* On jenkins, clear out old coverage reports so that we don't pick up last
  time's.
2017-02-15 16:52:19 +00:00
Kegan Dougal 9fdeb7a8e3 Add tests for the SyncAccumulator 2017-02-15 16:21:06 +00:00
David Baker 635b3dbccb Merge pull request #367 from matrix-org/dbkr/event_sender_docs
Add docs to event
2017-02-15 15:01:40 +00:00
David Baker e42af06609 spelling 2017-02-15 14:53:58 +00:00
David Baker 0416816329 Add docs to event
to say that sender won't always be set
2017-02-15 14:27:38 +00:00
David Baker 7f8f216263 Merge pull request #366 from matrix-org/rav/update_synctoken_more_often
Keep the device-sync token more up-to-date
2017-02-15 13:10:32 +00:00
Kegan Dougal 69ebcf08fc Bugfix where, upon refresh, it was impossible to write to the db (clone errors) 2017-02-14 17:28:56 +00:00
Kegan Dougal cbdb007b26 Persist receipt data 2017-02-14 16:51:27 +00:00
Kegan Dougal a28c03a2f9 Remove TODO checks and change copyright
Manually tested and it appears that Synapse will indeed send
down the complete state upon rejoining a room. \o/
2017-02-14 15:42:53 +00:00
David Baker 94a8915f6c Merge pull request #365 from matrix-org/rav/fix_devicelist_race_1
Fix race conditions in device list download
2017-02-14 10:46:48 +00:00
Richard van der Hoff be7192082a Keep the device-sync token more up-to-date
Update the sync token whenever the device list is in sync. Fixes
https://github.com/vector-im/riot-web/issues/3127.
2017-02-13 18:56:49 +00:00
Richard van der Hoff 54297cacd1 Fix race when downloading initial device change list
Fixes a problem if the user initiates a device query before the /changes
response comes back.
2017-02-13 18:33:02 +00:00
Richard van der Hoff 82f5997e61 Fix race condition in device list query
Fix a race where device list queries completing out-of-order could lead to us
thinking that we were more in-sync than we actually were.
2017-02-13 18:33:02 +00:00
David Baker 4d2bc88305 Merge pull request #364 from matrix-org/dbkr/fix_unban
Fix the unban method
2017-02-13 18:06:35 +00:00
David Baker cd8dfa331a Fix the unban method
Which didn't work because of
https://github.com/matrix-org/synapse/issues/1860
2017-02-13 18:00:28 +00:00
Richard van der Hoff 45d22c6196 Merge pull request #362 from matrix-org/rav/no_freeze_on_device_download
Spread out device verification work
2017-02-13 11:54:59 +00:00
Richard van der Hoff eeed11e283 Fix integ tests
Two tweaks:
 * `httpBackend.flush()` now returns a value, so we can't pass its result
   straight into `done()`.
 * In one of the megolm tests, we need to wait for the device query to finish
   before marking the relevant device as known. One easy way to do this is
   actually to try sending the message first - that will block until the device
   query completes.
2017-02-13 11:41:13 +00:00
Richard van der Hoff 476333b3fc Fix comment typo 2017-02-13 11:28:19 +00:00
Kegan Dougal c7fdbd1c64 Persist notification counts and treat them as clobbers 2017-02-10 17:15:17 +00:00
Kegan Dougal 0b9bc2a7a7 Remove old (de)serialize stuff. Inline more things 2017-02-10 16:51:55 +00:00
Kegan Dougal 6949b6666e Revert non-functional changes to other files 2017-02-10 16:13:13 +00:00
Kegan Dougal a380b6803a Merge branch 'develop' into kegan/indexeddb 2017-02-10 16:11:05 +00:00
Kegan Dougal 6696c712d3 Bug fixes with the accumualtor
- Only take invite_state as there is a Room attached to invite objects
  which then fail to be persisted.
- Store empty string state keys!
2017-02-10 15:19:50 +00:00
Kegan Dougal 0d4833d6e3 Make it all work! 2017-02-10 14:27:56 +00:00
Richard van der Hoff 207bce61ad Spread out device verification work
Avoid a big freeze when we process the results of a device query, by splitting
the work up by user.
2017-02-10 13:37:41 +00:00
Richard van der Hoff 0f1d367b80 Merge pull request #361 from matrix-org/rav/moar_uisi_logging
Clean up/improve e2e logging
2017-02-10 10:51:19 +00:00
Richard van der Hoff bf2e6a33c2 Minor post-review tweaks 2017-02-10 10:37:46 +00:00
Richard van der Hoff b66fed9ae9 Clean up/improve e2e logging
In an attempt to make the rageshake logs a bit more useful, try to make the
logging a bit saner. Firstly, make sure we log every decryption failure, and
log it exactly once, rather than in several places. Also record when we receive
megolm keys. Also add some more explicit logging in the sync loop.
2017-02-09 17:36:22 +00:00
Richard van der Hoff 4897287fda Merge pull request #360 from matrix-org/rav/fix_decrypt_after_keys_arrive
Fix decryption of events whose key arrives later
2017-02-09 17:33:45 +00:00
Kegan Dougal cc1daa5a54 Fix tests due to API breakage 2017-02-09 17:32:23 +00:00
Kegan Dougal 6e7b9472be Add a save() method to the store interface
Originally I just piggy-backed off setSyncToken() but this was]
non-obvious that depending on the store it might do writes to the
database. This was exacerbated by the fact that the save needs to
be done at the "right time" (after sync data is accumulated and
after the sync response has been processed) and setSyncToken was
being called too early.

A save() method fixes both these things.
2017-02-09 17:25:40 +00:00
Richard van der Hoff e13ed6436e Fix decryption of events whose key arrives later
Re-fixes https://github.com/vector-im/riot-web/issues/2273.

And test it this time.
2017-02-09 16:12:43 +00:00
Kegan Dougal db24690d9b Fix broken tests 2017-02-09 13:37:47 +00:00
Kegan Dougal 3a39fd23c4 Add glue code to hook up the sync accumulator
The user of the SDK is responsible for DIing the main components:

  let store = new IndexedDBStore(
    new IndexedDBStoreBackend(window.indexedDB),
    new SyncAccumulator(),
  });
  await store.startup();
  let client = matrix.createClient({store: store});
2017-02-09 12:49:10 +00:00
Richard van der Hoff d471277031 Merge pull request #359 from matrix-org/rav/remove_redundant_invalidate
Invalidate device lists when encryption is enabled in a room
2017-02-09 12:36:02 +00:00
Kegan Dougal ef57b88343 Remember next_batch tokens in the accumulator. Glue it in to SyncApi 2017-02-09 11:05:16 +00:00
Kegan Dougal 1e3fcdc109 Implement getJSON() 2017-02-09 10:50:31 +00:00
Kegan Dougal 606b9718a3 Implement timeline accumulation
Including pruning based on maxTimelineEntries
2017-02-09 10:14:10 +00:00
Kegan Dougal ab6e30da28 Merge branch 'develop' into kegan/indexeddb 2017-02-09 09:57:36 +00:00
Richard van der Hoff 0baea5c1a6 Invalidate device lists when encryption is enabled in a room
Fixes https://github.com/vector-im/riot-web/issues/2672
2017-02-08 23:23:46 +00:00
Richard van der Hoff bd07310e15 Remove redundant invalidation of our own device list
89ced198 added some code which flagged our own device list as in need of an
update. However, 8d502743 then added code such that we invalidate *all* members
of e2e rooms on the first initialsync - which should include ourselves. We can
therefore remove the redundant special-case, which mostly serves to simplify
the tests.
2017-02-08 23:04:23 +00:00
Richard van der Hoff bf227508ce matrix-client-crypto.spec: check no outstanding http expectations 2017-02-08 18:17:43 +00:00
Richard van der Hoff 1c1ba58579 Don't force device list download on every message in olm room
we only really hit this in the tests, but it's a bit silly to download the
complete device list on every message.
2017-02-08 18:10:01 +00:00
Richard van der Hoff a73f898bb9 try to make mock-request logging saner 2017-02-08 17:26:45 +00:00
Richard van der Hoff ee8a52d0c0 Merge pull request #358 from matrix-org/rav/mocha
Switch from jasmine to mocha + expect + lolex
2017-02-08 16:50:11 +00:00
Richard van der Hoff d5e87a537c Tell eslint we are using mocha rather than jasmine 2017-02-08 16:41:43 +00:00
Richard van der Hoff 18d3786676 Remove jasmine-node 2017-02-08 15:48:53 +00:00
Richard van der Hoff c906dbad75 jenkins.sh: clean out old reports 2017-02-08 15:14:17 +00:00
Kegan Dougal db20fc7831 Accumulate current room state 2017-02-08 15:08:38 +00:00
Richard van der Hoff bd226d94d8 Switch from jasmine to mocha + expect + lolex
Much of this transformation has been done automatically:
 * add expect import to each file
 * replace `not.to` with `toNot`
 * replace `to[Not]Be{Undefined,Null}` with equivalents
 * replace `jasmine.createSpy(...)` with `except.createSpy`, and `andCallFake`
   with `andCall`

Also:
 * replace `jasmine.createSpyObj` with manual alternatives
 * replace `jasmine.Clock` with `lolex`
2017-02-08 14:32:37 +00:00
Richard van der Hoff 8a487ca1bc Merge branch 'rav/source_map_support' into develop 2017-02-08 11:35:27 +00:00
David Baker 7b43a34860 Merge pull request #357 from matrix-org/rav/search_no_undefined_keys
searchMessageText: avoid setting keys=undefined
2017-02-08 10:58:44 +00:00
David Baker 6a60585123 Merge pull request #355 from matrix-org/rav/realtime_callbacks_this
realtime-callbacks: pass `global` as `this`
2017-02-08 10:57:06 +00:00
David Baker 0f7ab32777 Merge pull request #354 from matrix-org/rav/fix_tests_without_olm
Make the tests work without olm
2017-02-08 10:55:41 +00:00
Richard van der Hoff ffeaf2dec0 searchMessageText: avoid setting keys=undefined
This doesn't make any difference to the JSON, but it upsets `expect`.
2017-02-08 09:20:23 +00:00
Richard van der Hoff 3277820381 realtime-callbacks: pass global as this
We had a test for this, but apparently it wasn't testing it right...
2017-02-08 07:34:30 +00:00
Richard van der Hoff 80d0aadbd0 Install source-map-support in each test
This makes exception traces use the source map, which is much more helpful when
debugging.
2017-02-07 22:57:09 +00:00
Richard van der Hoff d744cecbfa Make the tests work without olm
Olm is an optional dependency, so the tests should work without it.
2017-02-07 22:56:56 +00:00
David Baker d453813db4 Merge pull request #353 from matrix-org/rav/refactor_e2e_tests
Tests: Factor out TestClient and use it in crypto tests
2017-02-06 16:45:52 +00:00
Kegan Dougal c06ebd1e4a Implement /sync accumulation for everything but room timelines/state 2017-02-06 16:45:25 +00:00
David Baker 10af2d0560 Merge pull request #352 from matrix-org/rav/device_list_lint
Fix some lint
2017-02-06 16:18:48 +00:00
Richard van der Hoff 42f2dafb40 Tests: Factor out TestClient and use it in crypto tests 2017-02-06 10:50:51 +00:00
Richard van der Hoff 6ca7661510 Fix some lint 2017-02-06 10:33:50 +00:00
David Baker 4388cc207f Merge pull request #351 from matrix-org/rav/sign_source_tarball
Make a sig for source tarballs when releasing
2017-02-06 11:32:45 +01:00
David Baker 07f14538e3 Merge pull request #350 from matrix-org/rav/no_merge_prerel
When doing a pre-release, don't bother merging to master and develop.
2017-02-06 11:28:46 +01:00
Richard van der Hoff 3d9173a877 Make a sig for source tarballs when releasing
When we are doing a signed release, also create a sig for the 'archive' tarball
which github creates for us.

Fixes https://github.com/vector-im/riot-web/issues/3024.
2017-02-04 11:10:40 +00:00
Richard van der Hoff cb88f53587 When doing a pre-release, don't bother merging to master and develop.
This assumes that we'll eventually do a proper release (or merge the prerel
manually), and saves twiddling the package.json on downstream projects for each
prerelease.
2017-02-04 11:07:14 +00:00
Richard van der Hoff d76e8be4ff Merge branch 'release-v0.7.5' 2017-02-04 10:22:04 +00:00
Kegan Dougal 0e4eebbb39 Add bare bones SyncAccumulator class with comments 2017-02-03 11:42:04 +00:00
Kegan Dougal f531b9fb21 Linting 2017-02-02 17:47:35 +00:00
Kegan Dougal 9581e48bcb Load/Store account data events 2017-02-02 17:41:49 +00:00
Kegan Dougal ad9d58ebc2 Persist User objects. Back out everything else.
We can reasonable persist User and account data objects. Other
objects get into horrible circular loops. We'll rethink how this
is being done.
2017-02-02 17:25:20 +00:00
Kegan Dougal 896fc5a3f0 Periodically update the database
Triggered by /sync responses, rather than timers, but with a minimum
delay time (1m).
2017-02-02 14:54:07 +00:00
Kegan Dougal 2c6d4c5a5c Implement deserialize() on all parts of the object graph 2017-02-02 12:38:03 +00:00
Kegan Dougal ef17214ae2 Add remaining load functions 2017-02-02 10:18:09 +00:00
Kegan Dougal 721b9df35d Add deserialize() static functions for User and MatrixEvent
The intent here is to make it possible to repopulate User objects from an
IndexedDB object store, which stores things according to the structured-clone
algorithm. We can't just `Object.assign` everything because it would assign
JSON objects to fields which should be classes (eg `MatrixEvent`).
2017-02-01 10:35:55 +00:00
Kegan Dougal 0317830b12 Check for a deserialize() function 2017-02-01 09:49:55 +00:00
Kegan Dougal dfd8c56838 Factor out upsert. Check for serialize() function on inserted objects 2017-02-01 09:42:20 +00:00
Kegan Dougal 025cb8bd91 Repopulate as User objects, not JSON objects 2017-01-31 17:33:12 +00:00
Kegan Dougal fa89f2be77 Start loading content from IndexedDB. Add idealised usage example. 2017-01-31 16:53:26 +00:00
Kegan Dougal bf008a1bee Fix build and add stub store connect() 2017-01-31 16:33:43 +00:00
Kegan Dougal 8656ad584b Introduce the concept of IndexedDBStoreBackend
So IndexedDBStore can meet the Store interface and we don't need to mess
with the guts of MatrixInMemoryStore.
2017-01-31 16:08:05 +00:00
Kegan Dougal e316a9a5b3 Merge branch 'develop' into kegan/indexeddb 2017-01-31 15:04:43 +00:00
Kegan Dougal 522105a858 First cut 2017-01-26 16:57:59 +00:00
Kegan Dougal 776cfed2b3 Merge branch 'develop' into kegan/indexeddb 2017-01-26 14:29:30 +00:00
David Baker 9c6973a46a v0.7.4 2017-01-16 13:03:35 +00:00
David Baker b439be8fb5 Prepare changelog for v0.7.4 2017-01-16 13:03:34 +00:00
David Baker edaf84a78f Fix non-screensharing calls 2017-01-16 12:28:52 +00:00
Kegan Dougal e159e504fa Use ev.target.result for consistency 2017-01-13 17:57:41 +00:00
Kegan Dougal 8bcb048f53 Very rough WIP of an IndexedDBStore 2017-01-13 16:22:59 +00:00
David Baker bb7c9b2227 v0.7.4-rc.1 2017-01-13 10:57:51 +00:00
David Baker 8538d7881a Prepare changelog for v0.7.4-rc.1 2017-01-13 10:57:50 +00:00
96 changed files with 16511 additions and 4178 deletions
+11 -1
View File
@@ -1,3 +1,13 @@
{
"presets": ["es2015"]
"presets": ["es2015"],
"plugins": [
// this transforms async functions into generator functions, which
// are then made to use the regenerator module by babel's
// transform-regnerator plugin (which is enabled by es2015).
"transform-async-to-bluebird",
// This makes sure that the regenerator runtime is available to
// the transpiled code.
"transform-runtime",
],
}
+23
View File
@@ -0,0 +1,23 @@
# Copyright 2017 Aviral Dasgupta
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
root = true
[*]
charset=utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
+4
View File
@@ -9,6 +9,10 @@ module.exports = {
env: {
browser: true,
node: true,
// babel's transform-runtime converts references to ES6 globals such as
// Promise and Map to core-js polyfills, so we can use ES6 globals.
es6: true,
},
extends: ["eslint:recommended", "google"],
rules: {
+3 -1
View File
@@ -1,4 +1,6 @@
.jsdoc
/.jsdocbuild
/.jsdoc
node_modules
.lock-wscript
build/Release
+1 -2
View File
@@ -2,5 +2,4 @@ language: node_js
node_js:
- node # Latest stable version of nodejs.
script:
- npm run lint
- npm run test
- ./travis.sh
+874
View File
@@ -1,3 +1,877 @@
Changes in [0.12.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.0-rc.1) (2018-10-11)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.1...v0.12.0-rc.1)
* never replace /sync'ed memberships with OOB ones
[\#760](https://github.com/matrix-org/matrix-js-sdk/pull/760)
* Don't fail to start up if lazy load check fails
[\#759](https://github.com/matrix-org/matrix-js-sdk/pull/759)
* Make e2e work on Edge
[\#754](https://github.com/matrix-org/matrix-js-sdk/pull/754)
* throw error with same name and message over idb worker boundary
[\#758](https://github.com/matrix-org/matrix-js-sdk/pull/758)
* Default to a room version of 1 when there is no room create event
[\#755](https://github.com/matrix-org/matrix-js-sdk/pull/755)
* Silence bluebird warnings
[\#757](https://github.com/matrix-org/matrix-js-sdk/pull/757)
* allow non-ff merge from release branch into master
[\#750](https://github.com/matrix-org/matrix-js-sdk/pull/750)
* Reject with the actual error on indexeddb error
[\#751](https://github.com/matrix-org/matrix-js-sdk/pull/751)
* Update mocha to v5
[\#744](https://github.com/matrix-org/matrix-js-sdk/pull/744)
* disable lazy loading for guests as they cant create filters
[\#748](https://github.com/matrix-org/matrix-js-sdk/pull/748)
* Revert "Add getMediaLimits to client"
[\#745](https://github.com/matrix-org/matrix-js-sdk/pull/745)
Latest Changes:
===============
BREAKING CHANGES
----------------
* If js-sdk finds data in the store that is incompatible with the options currently being used,
it will emit sync state ERROR with an error of type InvalidStoreError. It will also stop trying
to sync in this situation: the app must stop the client and then either clear the store or
change the options (in this case, enable or disable lazy loading of members) and then start
the client again.
Changes in [0.11.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.1) (2018-10-01)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.1-rc.1...v0.11.1)
* No changes since rc.1
Changes in [0.11.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.1-rc.1) (2018-09-27)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.0...v0.11.1-rc.1)
* make usage of hub compatible with latest version (2.5)
[\#747](https://github.com/matrix-org/matrix-js-sdk/pull/747)
* Detect when lazy loading has been toggled in client.startClient
[\#746](https://github.com/matrix-org/matrix-js-sdk/pull/746)
* Add getMediaLimits to client
[\#644](https://github.com/matrix-org/matrix-js-sdk/pull/644)
* Split npm start into an init and watch script
[\#742](https://github.com/matrix-org/matrix-js-sdk/pull/742)
* Revert "room name should only take canonical alias into account"
[\#738](https://github.com/matrix-org/matrix-js-sdk/pull/738)
* fix display name disambiguation with LL
[\#737](https://github.com/matrix-org/matrix-js-sdk/pull/737)
* Introduce Room.myMembership event
[\#735](https://github.com/matrix-org/matrix-js-sdk/pull/735)
* room name should only take canonical alias into account
[\#733](https://github.com/matrix-org/matrix-js-sdk/pull/733)
* state events from context response were not wrapped in a MatrixEvent
[\#732](https://github.com/matrix-org/matrix-js-sdk/pull/732)
* Reduce amount of promises created when inserting members
[\#724](https://github.com/matrix-org/matrix-js-sdk/pull/724)
* dont wait for LL members to be stored to resolve the members
[\#726](https://github.com/matrix-org/matrix-js-sdk/pull/726)
* RoomState.members emitted with wrong argument order for OOB members
[\#728](https://github.com/matrix-org/matrix-js-sdk/pull/728)
Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (2018-09-10)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.0-rc.1...v0.11.0)
BREAKING CHANGES
----------------
* v0.11.0-rc.1 introduced some breaking changes - see the respective release notes.
No changes since rc.1
Changes in [0.11.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0-rc.1) (2018-09-07)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9...v0.11.0-rc.1)
* Support for lazy loading members. This should improve performance for
users who joined big rooms a lot. Pass to `lazyLoadMembers = true` option when calling `startClient`.
BREAKING CHANGES
----------------
* `MatrixClient::startClient` now returns a Promise. No method should be called on the client before that promise resolves. Before this method didn't return anything.
* A new `CATCHUP` sync state, emitted by `MatrixClient#"sync"` and returned by `MatrixClient::getSyncState()`, when doing initial sync after the `ERROR` state. See `MatrixClient` documentation for details.
* `RoomState::maySendEvent('m.room.message', userId)` & `RoomState::maySendMessage(userId)` do not check the membership of the user anymore, only the power level. To check if the syncing user is allowed to write in a room, use `Room::maySendMessage()` as `RoomState` is not always aware of the syncing user's membership anymore, in case lazy loading of members is enabled.
All Changes
-----------
* Only emit CATCHUP if recovering from conn error
[\#727](https://github.com/matrix-org/matrix-js-sdk/pull/727)
* Fix docstring for sync data.error
[\#725](https://github.com/matrix-org/matrix-js-sdk/pull/725)
* Re-apply "Don't rely on members to query if syncing user can post to room"
[\#723](https://github.com/matrix-org/matrix-js-sdk/pull/723)
* Revert "Don't rely on members to query if syncing user can post to room"
[\#721](https://github.com/matrix-org/matrix-js-sdk/pull/721)
* Don't rely on members to query if syncing user can post to room
[\#717](https://github.com/matrix-org/matrix-js-sdk/pull/717)
* Fixes for room.guessDMUserId
[\#719](https://github.com/matrix-org/matrix-js-sdk/pull/719)
* Fix filepanel also filtering main timeline with LL turned on.
[\#716](https://github.com/matrix-org/matrix-js-sdk/pull/716)
* Remove lazy loaded members when leaving room
[\#711](https://github.com/matrix-org/matrix-js-sdk/pull/711)
* Fix: show spinner again while recovering from connection error
[\#702](https://github.com/matrix-org/matrix-js-sdk/pull/702)
* Add method to query LL state in client
[\#714](https://github.com/matrix-org/matrix-js-sdk/pull/714)
* Fix: also load invited members when lazy loading members
[\#707](https://github.com/matrix-org/matrix-js-sdk/pull/707)
* Pass through function to discard megolm session
[\#704](https://github.com/matrix-org/matrix-js-sdk/pull/704)
Changes in [0.10.9](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.9) (2018-09-03)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9-rc.2...v0.10.9)
* No changes since rc.2
Changes in [0.10.9-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.9-rc.2) (2018-08-31)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9-rc.1...v0.10.9-rc.2)
* Fix for "otherMember.getAvatarUrl is not a function"
[\#708](https://github.com/matrix-org/matrix-js-sdk/pull/708)
Changes in [0.10.9-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.9-rc.1) (2018-08-30)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.8...v0.10.9-rc.1)
* Fix DM avatar
[\#706](https://github.com/matrix-org/matrix-js-sdk/pull/706)
* Lazy loading: avoid loading members at initial sync for e2e rooms
[\#699](https://github.com/matrix-org/matrix-js-sdk/pull/699)
* Improve setRoomEncryption guard against multiple m.room.encryption st…
[\#700](https://github.com/matrix-org/matrix-js-sdk/pull/700)
* Revert "Lazy loading: don't block on setting up room crypto"
[\#698](https://github.com/matrix-org/matrix-js-sdk/pull/698)
* Lazy loading: don't block on setting up room crypto
[\#696](https://github.com/matrix-org/matrix-js-sdk/pull/696)
* Add getVisibleRooms()
[\#695](https://github.com/matrix-org/matrix-js-sdk/pull/695)
* Add wrapper around getJoinedMemberCount()
[\#697](https://github.com/matrix-org/matrix-js-sdk/pull/697)
* Api to fetch events via /room/.../event/..
[\#694](https://github.com/matrix-org/matrix-js-sdk/pull/694)
* Support for room upgrades
[\#693](https://github.com/matrix-org/matrix-js-sdk/pull/693)
* Lazy loading of room members
[\#691](https://github.com/matrix-org/matrix-js-sdk/pull/691)
Changes in [0.10.8](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.8) (2018-08-20)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.8-rc.1...v0.10.8)
* No changes since rc.1
Changes in [0.10.8-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.8-rc.1) (2018-08-16)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.7...v0.10.8-rc.1)
* Add getVersion to Room
[\#689](https://github.com/matrix-org/matrix-js-sdk/pull/689)
* Add getSyncStateData()
[\#680](https://github.com/matrix-org/matrix-js-sdk/pull/680)
* Send sync error to listener
[\#679](https://github.com/matrix-org/matrix-js-sdk/pull/679)
* make sure room.tags is always a valid object to avoid crashes
[\#675](https://github.com/matrix-org/matrix-js-sdk/pull/675)
* Fix infinite spinner upon joining a room
[\#673](https://github.com/matrix-org/matrix-js-sdk/pull/673)
Changes in [0.10.7](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.7) (2018-07-30)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.7-rc.1...v0.10.7)
* No changes since rc.1
Changes in [0.10.7-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.7-rc.1) (2018-07-24)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.6...v0.10.7-rc.1)
* encrypt for invited users if history visibility allows.
[\#666](https://github.com/matrix-org/matrix-js-sdk/pull/666)
Changes in [0.10.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.6) (2018-07-09)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.6-rc.1...v0.10.6)
* No changes since rc.1
Changes in [0.10.6-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.6-rc.1) (2018-07-06)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.5...v0.10.6-rc.1)
* Expose event decryption error via Event.decrypted event
[\#665](https://github.com/matrix-org/matrix-js-sdk/pull/665)
* Add decryption error codes to base.DecryptionError
[\#663](https://github.com/matrix-org/matrix-js-sdk/pull/663)
Changes in [0.10.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.5) (2018-06-29)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.5-rc.1...v0.10.5)
* No changes since rc.1
Changes in [0.10.5-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.5-rc.1) (2018-06-21)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.4...v0.10.5-rc.1)
* fix auth header and filename=undefined
[\#659](https://github.com/matrix-org/matrix-js-sdk/pull/659)
* allow setting the output device for webrtc calls
[\#650](https://github.com/matrix-org/matrix-js-sdk/pull/650)
* arguments true and false are actually invalid
[\#596](https://github.com/matrix-org/matrix-js-sdk/pull/596)
* fix typo where `headers` was not being used and thus sent wrong content-type
[\#643](https://github.com/matrix-org/matrix-js-sdk/pull/643)
* fix some documentation typos
[\#642](https://github.com/matrix-org/matrix-js-sdk/pull/642)
Changes in [0.10.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.4) (2018-06-12)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.4-rc.1...v0.10.4)
* No changes since rc.1
Changes in [0.10.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.4-rc.1) (2018-06-06)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.3...v0.10.4-rc.1)
* check whether notif level is undefined, because `0` is falsey
[\#651](https://github.com/matrix-org/matrix-js-sdk/pull/651)
Changes in [0.10.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.3) (2018-05-25)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.3-rc.1...v0.10.3)
* No changes since v0.10.3-rc.1
Changes in [0.10.3-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.3-rc.1) (2018-05-24)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.2...v0.10.3-rc.1)
BREAKING CHANGE
---------------
The deprecated 'callback' parameter has been removed from MatrixBaseApis.deactivateAccount
* Add `erase` option to deactivateAccount
[\#649](https://github.com/matrix-org/matrix-js-sdk/pull/649)
* Emit Session.no_consent when M_CONSENT_NOT_GIVEN received
[\#647](https://github.com/matrix-org/matrix-js-sdk/pull/647)
Changes in [0.10.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.2) (2018-04-30)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.2-rc.1...v0.10.2)
* No changes from rc.1
Changes in [0.10.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.2-rc.1) (2018-04-25)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.1...v0.10.2-rc.1)
* Ignore inserts of dup inbound group sessions, pt 2
[\#641](https://github.com/matrix-org/matrix-js-sdk/pull/641)
* Ignore inserts of duplicate inbound group sessions
[\#639](https://github.com/matrix-org/matrix-js-sdk/pull/639)
* Log IDB errors
[\#638](https://github.com/matrix-org/matrix-js-sdk/pull/638)
* Remove not very useful but veryv spammy log line
[\#632](https://github.com/matrix-org/matrix-js-sdk/pull/632)
* Switch event type to m.sticker.
[\#628](https://github.com/matrix-org/matrix-js-sdk/pull/628)
Changes in [0.10.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.1) (2018-04-12)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.0...v0.10.1)
* Log IDB errors
[\#638](https://github.com/matrix-org/matrix-js-sdk/pull/638)
* Ignore inserts of duplicate inbound group sessions
[\#639](https://github.com/matrix-org/matrix-js-sdk/pull/639)
* Ignore inserts of dup inbound group sessions, pt 2
[\#641](https://github.com/matrix-org/matrix-js-sdk/pull/641)
Changes in [0.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.0) (2018-04-11)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.0-rc.2...v0.10.0)
* No changes
Changes in [0.10.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.0-rc.2) (2018-04-09)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.0-rc.1...v0.10.0-rc.2)
* Add wrapper for group join API
* Add wrapped API to set group join\_policy
Changes in [0.10.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.0-rc.1) (2018-03-19)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.9.2...v0.10.0-rc.1)
* Fix duplicated state events in timeline from peek
[\#630](https://github.com/matrix-org/matrix-js-sdk/pull/630)
* Create indexeddb worker when starting the store
[\#627](https://github.com/matrix-org/matrix-js-sdk/pull/627)
* Fix indexeddb logging
[\#626](https://github.com/matrix-org/matrix-js-sdk/pull/626)
* Don't do /keys/changes on incremental sync
[\#625](https://github.com/matrix-org/matrix-js-sdk/pull/625)
* Don't mark devicelist dirty unnecessarily
[\#623](https://github.com/matrix-org/matrix-js-sdk/pull/623)
* Cache the joined member count for a room state
[\#619](https://github.com/matrix-org/matrix-js-sdk/pull/619)
* Fix JS doc
[\#618](https://github.com/matrix-org/matrix-js-sdk/pull/618)
* Precompute push actions for state events
[\#617](https://github.com/matrix-org/matrix-js-sdk/pull/617)
* Fix bug where global "Never send to unverified..." is ignored
[\#616](https://github.com/matrix-org/matrix-js-sdk/pull/616)
* Intern legacy top-level 'membership' field
[\#615](https://github.com/matrix-org/matrix-js-sdk/pull/615)
* Don't synthesize RR for m.room.redaction as causes the RR to go missing.
[\#598](https://github.com/matrix-org/matrix-js-sdk/pull/598)
* Make Events create Dates on demand
[\#613](https://github.com/matrix-org/matrix-js-sdk/pull/613)
* Stop cloning events when adding to state
[\#612](https://github.com/matrix-org/matrix-js-sdk/pull/612)
* De-dup code: use the initialiseState function
[\#611](https://github.com/matrix-org/matrix-js-sdk/pull/611)
* Create sentinel members on-demand
[\#610](https://github.com/matrix-org/matrix-js-sdk/pull/610)
* Some more doc on how sentinels work
[\#609](https://github.com/matrix-org/matrix-js-sdk/pull/609)
* Migrate room encryption store to crypto store
[\#597](https://github.com/matrix-org/matrix-js-sdk/pull/597)
* add parameter to getIdentityServerUrl to strip the protocol for invites
[\#600](https://github.com/matrix-org/matrix-js-sdk/pull/600)
* Move Device Tracking Data to Crypto Store
[\#594](https://github.com/matrix-org/matrix-js-sdk/pull/594)
* Optimise pushprocessor
[\#591](https://github.com/matrix-org/matrix-js-sdk/pull/591)
* Set event error before emitting
[\#592](https://github.com/matrix-org/matrix-js-sdk/pull/592)
* Add event type for stickers [WIP]
[\#590](https://github.com/matrix-org/matrix-js-sdk/pull/590)
* Migrate inbound sessions to cryptostore
[\#587](https://github.com/matrix-org/matrix-js-sdk/pull/587)
* Disambiguate names if they contain an mxid
[\#588](https://github.com/matrix-org/matrix-js-sdk/pull/588)
* Check for sessions in indexeddb before migrating
[\#585](https://github.com/matrix-org/matrix-js-sdk/pull/585)
* Emit an event for crypto store migration
[\#586](https://github.com/matrix-org/matrix-js-sdk/pull/586)
* Supporting fixes For making UnknownDeviceDialog not pop up automatically
[\#575](https://github.com/matrix-org/matrix-js-sdk/pull/575)
* Move sessions to the crypto store
[\#584](https://github.com/matrix-org/matrix-js-sdk/pull/584)
* Change crypto store transaction API
[\#582](https://github.com/matrix-org/matrix-js-sdk/pull/582)
* Add some missed copyright notices
[\#581](https://github.com/matrix-org/matrix-js-sdk/pull/581)
* Move Olm account to IndexedDB
[\#579](https://github.com/matrix-org/matrix-js-sdk/pull/579)
* Fix logging of DecryptionErrors to be more useful
[\#580](https://github.com/matrix-org/matrix-js-sdk/pull/580)
* [BREAKING] Change the behaviour of the unverfied devices blacklist flag
[\#568](https://github.com/matrix-org/matrix-js-sdk/pull/568)
* Support set_presence=offline for syncing
[\#557](https://github.com/matrix-org/matrix-js-sdk/pull/557)
* Consider cases where the sender may not redact their own event
[\#556](https://github.com/matrix-org/matrix-js-sdk/pull/556)
Changes in [0.9.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.9.2) (2017-12-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.9.1...v0.9.2)
Changes in [0.9.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.9.1) (2017-11-17)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.9.0...v0.9.1)
* Fix the force TURN option
[\#577](https://github.com/matrix-org/matrix-js-sdk/pull/577)
Changes in [0.9.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.9.0) (2017-11-15)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.9.0-rc.1...v0.9.0)
Changes in [0.9.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.9.0-rc.1) (2017-11-10)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.5...v0.9.0-rc.1)
* Modify addRoomToGroup to allow setting isPublic, create alias
updateGroupRoomAssociation
[\#567](https://github.com/matrix-org/matrix-js-sdk/pull/567)
* Expose more functionality of pushprocessor
[\#565](https://github.com/matrix-org/matrix-js-sdk/pull/565)
* Function for working out notif trigger permission
[\#566](https://github.com/matrix-org/matrix-js-sdk/pull/566)
* keep track of event ID and timestamp of decrypted messages
[\#555](https://github.com/matrix-org/matrix-js-sdk/pull/555)
* Fix notifEvent computation
[\#564](https://github.com/matrix-org/matrix-js-sdk/pull/564)
* Fix power level of sentinel members
[\#563](https://github.com/matrix-org/matrix-js-sdk/pull/563)
* don't try to decrypt a redacted message (fixes vector-im/riot-web#3744)
[\#554](https://github.com/matrix-org/matrix-js-sdk/pull/554)
* Support room notifs
[\#562](https://github.com/matrix-org/matrix-js-sdk/pull/562)
* Fix the glob-to-regex code
[\#558](https://github.com/matrix-org/matrix-js-sdk/pull/558)
Changes in [0.8.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.5) (2017-10-16)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.5-rc.1...v0.8.5)
* Make unknown pushrule conditions not match
[\#559](https://github.com/matrix-org/matrix-js-sdk/pull/559)
Changes in [0.8.5-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.5-rc.1) (2017-10-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.4...v0.8.5-rc.1)
* Implement wrapper API for removing a room from a group
[\#553](https://github.com/matrix-org/matrix-js-sdk/pull/553)
* Fix typo which resulted in stuck key download requests
[\#552](https://github.com/matrix-org/matrix-js-sdk/pull/552)
* Store group when it's created
[\#549](https://github.com/matrix-org/matrix-js-sdk/pull/549)
* Luke/groups remove rooms users from summary
[\#548](https://github.com/matrix-org/matrix-js-sdk/pull/548)
* Clean on prepublish
[\#546](https://github.com/matrix-org/matrix-js-sdk/pull/546)
* Implement wrapper APIs for adding rooms to group summary
[\#545](https://github.com/matrix-org/matrix-js-sdk/pull/545)
Changes in [0.8.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.4) (2017-09-21)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.3...v0.8.4)
* Fix build issue
Changes in [0.8.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.3) (2017-09-20)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.3-rc.1...v0.8.3)
* No changes
Changes in [0.8.3-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.3-rc.1) (2017-09-19)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.2...v0.8.3-rc.1)
* consume trailing slash when creating Matrix Client in HS and IS urls
[\#526](https://github.com/matrix-org/matrix-js-sdk/pull/526)
* Add ignore users API
[\#539](https://github.com/matrix-org/matrix-js-sdk/pull/539)
* Upgrade to jsdoc 3.5.5
[\#540](https://github.com/matrix-org/matrix-js-sdk/pull/540)
* Make re-emitting events much more memory efficient
[\#538](https://github.com/matrix-org/matrix-js-sdk/pull/538)
* Only re-emit events from Event objects if needed
[\#536](https://github.com/matrix-org/matrix-js-sdk/pull/536)
* Handle 'left' users in the deviceList mananagement
[\#535](https://github.com/matrix-org/matrix-js-sdk/pull/535)
* Factor out devicelist integration tests to a separate file
[\#534](https://github.com/matrix-org/matrix-js-sdk/pull/534)
* Refactor sync._sync as an async function
[\#533](https://github.com/matrix-org/matrix-js-sdk/pull/533)
* Add es6 to eslint environments
[\#532](https://github.com/matrix-org/matrix-js-sdk/pull/532)
Changes in [0.8.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.2) (2017-08-24)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.1...v0.8.2)
* Handle m.call.* events which are decrypted asynchronously
[\#530](https://github.com/matrix-org/matrix-js-sdk/pull/530)
* Re-emit events from, er, Event objects
[\#529](https://github.com/matrix-org/matrix-js-sdk/pull/529)
Changes in [0.8.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.1) (2017-08-23)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.1-rc.1...v0.8.1)
* [No changes]
Changes in [0.8.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.1-rc.1) (2017-08-22)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.0...v0.8.1-rc.1)
* Fix error handling in interactive-auth
[\#527](https://github.com/matrix-org/matrix-js-sdk/pull/527)
* Make lots of OlmDevice asynchronous
[\#524](https://github.com/matrix-org/matrix-js-sdk/pull/524)
* Make crypto.decryptMessage return decryption results
[\#523](https://github.com/matrix-org/matrix-js-sdk/pull/523)
Changes in [0.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.0) (2017-08-15)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.13...v0.8.0)
BREAKING CHANGE
---------------
In order to support a move to a more scalable storage backend, we need to make
a number of the APIs related end-to-end encryption asynchronous.
This release of the JS-SDK includes the following changes which will affect
applications which support end-to-end encryption:
1. `MatrixClient` now provides a new (asynchronous) method,
`initCrypto`. Applications which support end-to-end encryption must call
this method (and wait for it to complete) before calling `startClient`, to
give the crypto layer a chance to initialise.
2. The following APIs have been changed to return promises:
* `MatrixClient.getStoredDevicesForUser`
* `MatrixClient.getStoredDevice`
* `MatrixClient.setDeviceVerified`
* `MatrixClient.setDeviceBlocked`
* `MatrixClient.setDeviceKnown`
* `MatrixClient.getEventSenderDeviceInfo`
* `MatrixClient.isEventSenderVerified`
* `MatrixClient.importRoomKeys`
Applications using the results of any of the above methods will need to be
updated to wait for the result of the promise.
3. `MatrixClient.listDeviceKeys` has been removed altogether. It's been
deprecated for some time. Applications using it should instead be changed to
use `MatrixClient.getStoredDevices`, which is similar but returns its results
in a slightly different format.
* Make bits of `olmlib` asynchronous
[\#521](https://github.com/matrix-org/matrix-js-sdk/pull/521)
* Make some of DeviceList asynchronous
[\#520](https://github.com/matrix-org/matrix-js-sdk/pull/520)
* Make methods in crypto/algorithms async
[\#519](https://github.com/matrix-org/matrix-js-sdk/pull/519)
* Avoid sending unencrypted messages in e2e room
[\#518](https://github.com/matrix-org/matrix-js-sdk/pull/518)
* Make tests wait for syncs to happen
[\#517](https://github.com/matrix-org/matrix-js-sdk/pull/517)
* Make a load of methods in the 'Crypto' module asynchronous
[\#510](https://github.com/matrix-org/matrix-js-sdk/pull/510)
* Set `rawDisplayName` to `userId` if membership has `displayname=null`
[\#515](https://github.com/matrix-org/matrix-js-sdk/pull/515)
* Refactor handling of crypto events for async
[\#508](https://github.com/matrix-org/matrix-js-sdk/pull/508)
* Let event decryption be asynchronous
[\#509](https://github.com/matrix-org/matrix-js-sdk/pull/509)
* Transform `async` functions to bluebird promises
[\#511](https://github.com/matrix-org/matrix-js-sdk/pull/511)
* Add more group APIs
[\#512](https://github.com/matrix-org/matrix-js-sdk/pull/512)
* Retrying test: wait for localEchoUpdated event
[\#507](https://github.com/matrix-org/matrix-js-sdk/pull/507)
* Fix member events breaking on timeline reset, 2
[\#504](https://github.com/matrix-org/matrix-js-sdk/pull/504)
* Make bits of the js-sdk api asynchronous
[\#503](https://github.com/matrix-org/matrix-js-sdk/pull/503)
* Yet more js-sdk test deflakification
[\#499](https://github.com/matrix-org/matrix-js-sdk/pull/499)
* Fix racy 'matrixclient retrying' test
[\#497](https://github.com/matrix-org/matrix-js-sdk/pull/497)
* Fix spamming of key-share-requests
[\#495](https://github.com/matrix-org/matrix-js-sdk/pull/495)
* Add progress handler to `uploadContent`
[\#500](https://github.com/matrix-org/matrix-js-sdk/pull/500)
* Switch matrix-js-sdk to bluebird
[\#490](https://github.com/matrix-org/matrix-js-sdk/pull/490)
* Fix some more flakey tests
[\#492](https://github.com/matrix-org/matrix-js-sdk/pull/492)
* make the npm test script windows-friendly
[\#489](https://github.com/matrix-org/matrix-js-sdk/pull/489)
* Fix a bunch of races in the tests
[\#488](https://github.com/matrix-org/matrix-js-sdk/pull/488)
* Fix early return in MatrixClient.setGuestAccess
[\#487](https://github.com/matrix-org/matrix-js-sdk/pull/487)
* Remove testUtils.failTest
[\#486](https://github.com/matrix-org/matrix-js-sdk/pull/486)
* Add test:watch script
[\#485](https://github.com/matrix-org/matrix-js-sdk/pull/485)
* Make it possible to use async/await
[\#484](https://github.com/matrix-org/matrix-js-sdk/pull/484)
* Remove m.new_device support
[\#483](https://github.com/matrix-org/matrix-js-sdk/pull/483)
* Use access-token in header
[\#478](https://github.com/matrix-org/matrix-js-sdk/pull/478)
* Sanity-check response from /thirdparty/protocols
[\#482](https://github.com/matrix-org/matrix-js-sdk/pull/482)
* Avoid parsing plain-text errors as JSON
[\#479](https://github.com/matrix-org/matrix-js-sdk/pull/479)
* Use external mock-request
[\#481](https://github.com/matrix-org/matrix-js-sdk/pull/481)
* Fix some races in the tests
[\#480](https://github.com/matrix-org/matrix-js-sdk/pull/480)
* Fall back to MemoryCryptoStore if indexeddb fails
[\#475](https://github.com/matrix-org/matrix-js-sdk/pull/475)
* Fix load failure in firefox when indexedDB is disabled
[\#474](https://github.com/matrix-org/matrix-js-sdk/pull/474)
* Fix a race in a test
[\#471](https://github.com/matrix-org/matrix-js-sdk/pull/471)
* Avoid throwing an unhandled error when the indexeddb is deleted
[\#470](https://github.com/matrix-org/matrix-js-sdk/pull/470)
* fix jsdoc
[\#469](https://github.com/matrix-org/matrix-js-sdk/pull/469)
* Handle m.forwarded_room_key events
[\#468](https://github.com/matrix-org/matrix-js-sdk/pull/468)
* Improve error reporting from indexeddbstore.clearDatabase
[\#466](https://github.com/matrix-org/matrix-js-sdk/pull/466)
* Implement sharing of megolm keys
[\#454](https://github.com/matrix-org/matrix-js-sdk/pull/454)
* Process received room key requests
[\#449](https://github.com/matrix-org/matrix-js-sdk/pull/449)
* Send m.room_key_request events when we fail to decrypt an event
[\#448](https://github.com/matrix-org/matrix-js-sdk/pull/448)
Changes in [0.7.13](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.13) (2017-06-22)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.12...v0.7.13)
* Fix failure on Tor browser
[\#473](https://github.com/matrix-org/matrix-js-sdk/pull/473)
* Fix issues with firefox private browsing
[\#472](https://github.com/matrix-org/matrix-js-sdk/pull/472)
Changes in [0.7.12](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.12) (2017-06-19)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.12-rc.1...v0.7.12)
* No changes
Changes in [0.7.12-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.12-rc.1) (2017-06-15)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.11...v0.7.12-rc.1)
* allow setting iceTransportPolicy to relay through forceTURN option
[\#462](https://github.com/matrix-org/matrix-js-sdk/pull/462)
Changes in [0.7.11](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.11) (2017-06-12)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.11-rc.1...v0.7.11)
* Add a bunch of logging around sending messages
[\#460](https://github.com/matrix-org/matrix-js-sdk/pull/460)
Changes in [0.7.11-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.11-rc.1) (2017-06-09)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.10...v0.7.11-rc.1)
* Make TimelineWindow.load resolve quicker if we have the events
[\#458](https://github.com/matrix-org/matrix-js-sdk/pull/458)
* Stop peeking when a matrix client is stopped
[\#451](https://github.com/matrix-org/matrix-js-sdk/pull/451)
* Update README: Clarify how to install libolm
[\#450](https://github.com/matrix-org/matrix-js-sdk/pull/450)
Changes in [0.7.10](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.10) (2017-06-02)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.9...v0.7.10)
* BREAKING CHANGE: The SDK no longer ``require``s ``olm`` - instead it expects
libolm to be provided as an ``Olm`` global. This will only affect
applications which use end-to-end encryption. See the
[README](README.md#end-to-end-encryption-support) for details.
* indexeddb-crypto-store: fix db deletion
[\#447](https://github.com/matrix-org/matrix-js-sdk/pull/447)
* Load Olm from the global rather than requiring it.
[\#446](https://github.com/matrix-org/matrix-js-sdk/pull/446)
Changes in [0.7.9](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.9) (2017-06-01)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.8...v0.7.9)
* Initial framework for indexeddb-backed crypto store
[\#445](https://github.com/matrix-org/matrix-js-sdk/pull/445)
* Factor out reEmit to a common module
[\#444](https://github.com/matrix-org/matrix-js-sdk/pull/444)
* crypto/algorithms/base.js: Convert to es6
[\#443](https://github.com/matrix-org/matrix-js-sdk/pull/443)
* maySendRedactionForEvent for userId
[\#435](https://github.com/matrix-org/matrix-js-sdk/pull/435)
* MatrixClient: add getUserId()
[\#441](https://github.com/matrix-org/matrix-js-sdk/pull/441)
* Run jsdoc on a custom babeling of the source
[\#442](https://github.com/matrix-org/matrix-js-sdk/pull/442)
* Add in a public api getStoredDevice allowing clients to get a specific
device
[\#439](https://github.com/matrix-org/matrix-js-sdk/pull/439)
Changes in [0.7.8](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.8) (2017-05-22)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.8-rc.1...v0.7.8)
* No changes
Changes in [0.7.8-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.8-rc.1) (2017-05-19)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.7...v0.7.8-rc.1)
* Attempt to rework the release-tarball-signing stuff
[\#438](https://github.com/matrix-org/matrix-js-sdk/pull/438)
* ability to specify webrtc audio/video inputs for the lib to request
[\#427](https://github.com/matrix-org/matrix-js-sdk/pull/427)
* make screen sharing call FF friendly :D
[\#434](https://github.com/matrix-org/matrix-js-sdk/pull/434)
* Fix race in device list updates
[\#431](https://github.com/matrix-org/matrix-js-sdk/pull/431)
* WebRTC: Support recvonly for video for those without a webcam
[\#424](https://github.com/matrix-org/matrix-js-sdk/pull/424)
* Update istanbul to remove minimatch DoS Warning
[\#422](https://github.com/matrix-org/matrix-js-sdk/pull/422)
* webrtc/call: Make it much less likely that callIds collide locally
[\#423](https://github.com/matrix-org/matrix-js-sdk/pull/423)
* Automatically complete dummy auth
[\#420](https://github.com/matrix-org/matrix-js-sdk/pull/420)
* Don't leave the gh-pages branch checked out
[\#418](https://github.com/matrix-org/matrix-js-sdk/pull/418)
Changes in [0.7.7](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.7) (2017-04-25)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.7-rc.1...v0.7.7)
* No changes
Changes in [0.7.7-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.7-rc.1) (2017-04-21)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6...v0.7.7-rc.1)
* Automatically complete dummy auth
[\#420](https://github.com/matrix-org/matrix-js-sdk/pull/420)
Changes in [0.7.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.6) (2017-04-12)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6-rc.2...v0.7.6)
* No changes
Changes in [0.7.6-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.6-rc.2) (2017-04-10)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6-rc.1...v0.7.6-rc.2)
* Add feature detection for webworkers
[\#416](https://github.com/matrix-org/matrix-js-sdk/pull/416)
* Fix release script
[\#415](https://github.com/matrix-org/matrix-js-sdk/pull/415)
Changes in [0.7.6-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.6-rc.1) (2017-04-07)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5...v0.7.6-rc.1)
* Make indexeddb save after the first sync
[\#414](https://github.com/matrix-org/matrix-js-sdk/pull/414)
* Make indexeddb startup faster
[\#413](https://github.com/matrix-org/matrix-js-sdk/pull/413)
* Add ability to do indexeddb sync work in webworker
[\#412](https://github.com/matrix-org/matrix-js-sdk/pull/412)
* Move more functionality to the indexeddb backend
[\#409](https://github.com/matrix-org/matrix-js-sdk/pull/409)
* Indicate syncState ERROR after many failed /syncs
[\#410](https://github.com/matrix-org/matrix-js-sdk/pull/410)
* Further reorganising of indexeddb sync code
[\#407](https://github.com/matrix-org/matrix-js-sdk/pull/407)
* Change interface of IndexedDBStore: hide internals
[\#406](https://github.com/matrix-org/matrix-js-sdk/pull/406)
* Don't be SYNCING until updating from the server
[\#405](https://github.com/matrix-org/matrix-js-sdk/pull/405)
* Don't log the entire /sync response
[\#403](https://github.com/matrix-org/matrix-js-sdk/pull/403)
* webrtc/call: Assign MediaStream to video element srcObject
[\#402](https://github.com/matrix-org/matrix-js-sdk/pull/402)
* Fix undefined reference in http-api
[\#400](https://github.com/matrix-org/matrix-js-sdk/pull/400)
* Add copyright header to event-timeline.js
[\#382](https://github.com/matrix-org/matrix-js-sdk/pull/382)
* client: fix docs for user-scoped account_data events
[\#397](https://github.com/matrix-org/matrix-js-sdk/pull/397)
* Add a CONTRIBUTING for js-sdk
[\#399](https://github.com/matrix-org/matrix-js-sdk/pull/399)
* Fix leaking room state objects on limited sync responses
[\#395](https://github.com/matrix-org/matrix-js-sdk/pull/395)
* Extend 'ignoreFailure' to be 'background'
[\#396](https://github.com/matrix-org/matrix-js-sdk/pull/396)
* Add x_show_msisdn parameter to register calls
[\#388](https://github.com/matrix-org/matrix-js-sdk/pull/388)
* Update event redaction to keep sender and origin_server_ts
[\#394](https://github.com/matrix-org/matrix-js-sdk/pull/394)
* Handle 'limited' timeline responses in the SyncAccumulator
[\#393](https://github.com/matrix-org/matrix-js-sdk/pull/393)
* Give a better error message if the HS doesn't support msisdn registeration
[\#391](https://github.com/matrix-org/matrix-js-sdk/pull/391)
* Add getEmailSid
[\#383](https://github.com/matrix-org/matrix-js-sdk/pull/383)
* Add m.login.email.identity support to UI auth
[\#380](https://github.com/matrix-org/matrix-js-sdk/pull/380)
* src/client.js: Fix incorrect roomId reference in VoIP glare code
[\#381](https://github.com/matrix-org/matrix-js-sdk/pull/381)
* add .editorconfig
[\#379](https://github.com/matrix-org/matrix-js-sdk/pull/379)
* Store account data in the same way as room data
[\#377](https://github.com/matrix-org/matrix-js-sdk/pull/377)
* Upload one-time keys on /sync rather than a timer
[\#376](https://github.com/matrix-org/matrix-js-sdk/pull/376)
* Increase the WRITE_DELAY on database syncing
[\#374](https://github.com/matrix-org/matrix-js-sdk/pull/374)
* Make deleteAllData() return a Promise
[\#373](https://github.com/matrix-org/matrix-js-sdk/pull/373)
* Don't include banned users in the room name
[\#372](https://github.com/matrix-org/matrix-js-sdk/pull/372)
* Support IndexedDB as a backing store
[\#363](https://github.com/matrix-org/matrix-js-sdk/pull/363)
* Poll /sync with a short timeout while catching up
[\#370](https://github.com/matrix-org/matrix-js-sdk/pull/370)
* Make test coverage work again
[\#368](https://github.com/matrix-org/matrix-js-sdk/pull/368)
* Add docs to event
[\#367](https://github.com/matrix-org/matrix-js-sdk/pull/367)
* Keep the device-sync token more up-to-date
[\#366](https://github.com/matrix-org/matrix-js-sdk/pull/366)
* Fix race conditions in device list download
[\#365](https://github.com/matrix-org/matrix-js-sdk/pull/365)
* Fix the unban method
[\#364](https://github.com/matrix-org/matrix-js-sdk/pull/364)
* Spread out device verification work
[\#362](https://github.com/matrix-org/matrix-js-sdk/pull/362)
* Clean up/improve e2e logging
[\#361](https://github.com/matrix-org/matrix-js-sdk/pull/361)
* Fix decryption of events whose key arrives later
[\#360](https://github.com/matrix-org/matrix-js-sdk/pull/360)
* Invalidate device lists when encryption is enabled in a room
[\#359](https://github.com/matrix-org/matrix-js-sdk/pull/359)
* Switch from jasmine to mocha + expect + lolex
[\#358](https://github.com/matrix-org/matrix-js-sdk/pull/358)
* Install source-map-support in each test
[\#356](https://github.com/matrix-org/matrix-js-sdk/pull/356)
* searchMessageText: avoid setting keys=undefined
[\#357](https://github.com/matrix-org/matrix-js-sdk/pull/357)
* realtime-callbacks: pass `global` as `this`
[\#355](https://github.com/matrix-org/matrix-js-sdk/pull/355)
* Make the tests work without olm
[\#354](https://github.com/matrix-org/matrix-js-sdk/pull/354)
* Tests: Factor out TestClient and use it in crypto tests
[\#353](https://github.com/matrix-org/matrix-js-sdk/pull/353)
* Fix some lint
[\#352](https://github.com/matrix-org/matrix-js-sdk/pull/352)
* Make a sig for source tarballs when releasing
[\#351](https://github.com/matrix-org/matrix-js-sdk/pull/351)
* When doing a pre-release, don't bother merging to master and develop.
[\#350](https://github.com/matrix-org/matrix-js-sdk/pull/350)
Changes in [0.7.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5) (2017-02-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5-rc.3...v0.7.5)
+112 -1
View File
@@ -1,4 +1,115 @@
Contributing code to matrix-js-sdk
==================================
matrix-js-sdk follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
willing to license their contributions under the same license as the project
itself. We follow a simple 'inbound=outbound' model for contributions: the act
of submitting an 'inbound' contribution means that the contributor agrees to
license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see `<LICENSE>`_).
How to contribute
~~~~~~~~~~~~~~~~~
The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes
into our repo (https://help.github.com/articles/using-pull-requests/)
**The single biggest thing you need to know is: please base your changes on
the develop branch - /not/ master.**
We use the master branch to track the most recent release, so that folks who
blindly clone the repo and automatically check out master get something that
works. Develop is the unstable branch where all the development actually
happens: the workflow is that contributors should fork the develop branch to
make a 'feature' branch for a particular contribution, and then make a pull
request to merge this back into the matrix.org 'official' develop branch. We
use github's pull request workflow to review the contribution, and either ask
you to make any refinements needed or merge it and make them ourselves. The
changes will then land on master when we next do a release.
We use Travis for continuous integration, and all pull requests get
automatically tested by Travis: if your change breaks the build, then the PR
will show that there are failed checks, so please check back after a few
minutes.
Code style
~~~~~~~~~~
The code-style for matrix-js-sdk is not formally documented, but contributors
are encouraged to read the code style document for matrix-react-sdk
(`<https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md>`_)
and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project,
and **never** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise.
Attribution
~~~~~~~~~~~
Everyone who contributes anything to Matrix is welcome to be listed in the
AUTHORS.rst file for the project in question. Please feel free to include a
change to AUTHORS.rst in your pull request to list yourself and a short
description of the area(s) you've worked on. Also, we sometimes have swag to
give away to contributors - if you feel that Matrix-branded apparel is missing
from your life, please mail us your shipping address to matrix at matrix.org
and we'll try to fix it :)
Sign off
~~~~~~~~
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've adopted the
same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix::
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment::
Signed-off-by: Your Name <your@email.example.org>
...using your real name; unfortunately pseudonyms and anonymous contributions
can't be accepted. Git makes this trivial - just use the -s flag when you do
``git commit``, having first set ``user.name`` and ``user.email`` git configs
(which you should have done anyway :)
+44 -2
View File
@@ -13,7 +13,8 @@ In a browser
Download either the full or minified version from
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
``<script>`` to your page. There will be a global variable ``matrixcs``
attached to ``window`` through which you can access the SDK.
attached to ``window`` through which you can access the SDK. See below for how to
include libolm to enable end-to-end-encryption.
Please check [the working browser example](examples/browser) for more information.
@@ -29,8 +30,9 @@ In Node.js
console.log("Public Rooms: %s", JSON.stringify(data));
});
```
See below for how to include libolm to enable end-to-end-encryption. Please check
[the Node.js terminal app](examples/node) for a more complex example.
Please check [the Node.js terminal app](examples/node) for a more complex example.
What does this SDK do?
----------------------
@@ -236,6 +238,46 @@ host the API reference from the source files like this:
Then visit ``http://localhost:8005`` to see the API docs.
End-to-end encryption support
=============================
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
[libolm](http://matrix.org/git/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
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
initialise the crypto layer.
If the ``Olm`` global is not available, the SDK will show a warning, as shown
below; ``initCrypto()`` will also fail.
```
Unable to load crypto module: crypto will be disabled: Error: global.Olm is not defined
```
If the crypto layer is not (successfully) initialised, the SDK will continue to
work for unencrypted rooms, but it will not support the E2E parts of the Matrix
specification.
To provide the Olm library in a browser application:
* download the transpiled libolm (from https://matrix.org/packages/npm/olm/).
* load ``olm.js`` as a ``<script>`` *before* ``browser-matrix.js``.
To provide the Olm library in a node.js application:
* ``npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz``
(replace the URL with the latest version you want to use from
https://matrix.org/packages/npm/olm/)
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
If you want to package Olm as dependency for your node.js application, you
can use ``npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz
--save-optional`` (if your application also works without e2e crypto enabled)
or ``--save`` (if it doesn't) to do so.
Contributing
============
*This section is for people who want to modify the SDK. If you just
+19
View File
@@ -1,4 +1,23 @@
var matrixcs = require("./lib/matrix");
matrixcs.request(require("browser-request"));
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.
var indexedDB;
try {
indexedDB = global.indexedDB;
} catch(e) {}
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
if (indexedDB) {
matrixcs.setCryptoStoreFactory(
function() {
return new matrixcs.IndexedDBCryptoStore(
indexedDB, "matrix-js-sdk:crypto"
);
}
);
}
module.exports = matrixcs; // keep export for browserify package deps
global.matrixcs = matrixcs;
+3 -3
View File
@@ -202,9 +202,9 @@ function printRoomList() {
dateStr = new Date(msg.getTs()).toISOString().replace(
/T/, ' ').replace(/\..+/, '');
}
var me = roomList[i].getMember(myUserId);
if (me) {
fmt = fmts[me.membership];
var myMembership = roomList[i].getMyMembership();
if (myMembership) {
fmt = fmts[myMembership];
}
var roomName = fixWidth(roomList[i].name, 25);
print(
+1 -1
View File
@@ -44,7 +44,7 @@ window.onload = function() {
disableButtons(true, true, true);
};
matrixClient.on("sync", function(state, prevState, data) {
client.on("sync", function(state, prevState, data) {
switch (state) {
case "PREPARED":
syncComplete();
+4 -1
View File
@@ -2,7 +2,7 @@
set -x
export NVM_DIR="/home/jenkins/.nvm"
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 6 || exit $?
@@ -15,6 +15,9 @@ function fail {
RC=1
}
# don't use last time's test reports
rm -rf reports coverage || exit $?
npm test || fail "npm test finished with return code $?"
npm run -s lint -- -f checkstyle > eslint.xml ||
+28 -21
View File
@@ -1,19 +1,24 @@
{
"name": "matrix-js-sdk",
"version": "0.7.5",
"version": "0.12.0-rc.1",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"buildtest": "babel -s -d specbuild spec",
"test": "npm run buildtest && istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- specbuild --verbose --junitreport --captureExceptions",
"check": "npm run buildtest && jasmine-node specbuild --verbose --junitreport --captureExceptions",
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
"start": "babel -s -w -d lib src",
"test:build": "babel -s -d specbuild spec",
"test:run": "istanbul cover --report text --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" node_modules/mocha/bin/_mocha -- --recursive specbuild --colors --reporter mocha-jenkins-reporter --reporter-options junit_report_path=reports/test-results.xml",
"test:watch": "mocha --watch --compilers js:babel-core/register --recursive spec --colors",
"test": "npm run test:build && npm run test:run",
"check": "npm run test:build && _mocha --recursive specbuild --colors",
"gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
"start": "npm run start:init && npm run start:watch",
"start:watch": "babel -s -w --skip-initial-build -d lib src",
"start:init": "babel -s -d lib src",
"clean": "rimraf lib dist",
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js",
"dist": "npm run build",
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
"lint": "eslint --max-warnings 121 src spec",
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt"
"lint": "eslint --max-warnings 102 src spec",
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt"
},
"repository": {
"url": "https://github.com/matrix-org/matrix-js-sdk"
@@ -47,37 +52,39 @@
],
"dependencies": {
"another-json": "^0.2.0",
"babel-runtime": "^6.26.0",
"bluebird": "^3.5.0",
"browser-request": "^0.3.3",
"q": "^1.4.1",
"content-type": "^1.0.2",
"request": "^2.53.0"
},
"devDependencies": {
"babel-cli": "^6.18.0",
"babel-eslint": "^7.1.1",
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.18.0",
"browserify": "^14.0.0",
"browserify-shim": "^3.8.13",
"eslint": "^3.13.1",
"eslint-config-google": "^0.7.1",
"exorcist": "^0.4.0",
"istanbul": "^0.3.13",
"jasmine-node": "^1.14.5",
"jsdoc": "^3.4.0",
"expect": "^1.20.2",
"istanbul": "^0.4.5",
"jsdoc": "^3.5.5",
"lolex": "^1.5.2",
"matrix-mock-request": "^1.2.0",
"mocha": "^5.2.0",
"mocha-jenkins-reporter": "^0.4.0",
"rimraf": "^2.5.4",
"source-map-support": "^0.4.11",
"sourceify": "^0.1.0",
"uglifyjs": "^2.4.10",
"uglify-js": "^2.8.26",
"watchify": "^3.2.1"
},
"optionalDependencies": {
"olm": "https://matrix.org/packages/npm/olm/olm-2.2.1.tgz"
},
"browserify": {
"transform": [
"sourceify",
"browserify-shim"
"sourceify"
]
},
"browserify-shim": {
"olm": "global:Olm"
}
}
+60 -11
View File
@@ -178,10 +178,6 @@ if [ $dodist -eq 0 ]; then
done
fi
# push the release branch (github can't release from
# a branch it doesn't have)
git push origin "$rel_branch"
if [ -n "$signing_id" ]; then
# make a signed tag
# gnupg seems to fail to get the right tty device unless we set it here
@@ -190,8 +186,55 @@ else
git tag -a -F "${latest_changes}" "$tag"
fi
# push the tag
git push origin "$tag"
# push the tag and the release branch
git push origin "$rel_branch" "$tag"
if [ -n "$signing_id" ]; then
# make a signature for the source tarball.
#
# github will make us a tarball from the tag - we want to create a
# signature for it, which means that first of all we need to check that
# it's correct.
#
# we can't deterministically build exactly the same tarball, due to
# differences in gzip implementation - but we *can* build the same tar - so
# the easiest way to check the validity of the tarball from git is to unzip
# it and compare it with our own idea of what the tar should look like.
# the name of the sig file we want to create
source_sigfile="${tag}-src.tar.gz.asc"
tarfile="$tag.tar.gz"
gh_project_url=$(git remote get-url origin |
sed -e 's#^git@github\.com:#https://github.com/#' \
-e 's#^git\+ssh://git@github\.com/#https://github.com/#' \
-e 's/\.git$//')
project_name="${gh_project_url##*/}"
curl -L "${gh_project_url}/archive/${tarfile}" -o "${tarfile}"
# unzip it and compare it with the tar we would generate
if ! cmp --silent <(gunzip -c $tarfile) \
<(git archive --format tar --prefix="${project_name}-${release}/" "$tag"); then
# we don't bail out here, because really it's more likely that our comparison
# screwed up and it's super annoying to abort the script at this point.
cat >&2 <<EOF
!!!!!!!!!!!!!!!!!
!!!! WARNING !!!!
Mismatch between our own tarfile and that generated by github: not signing
source tarball.
To resolve, determine if $tarfile is correct, and if so sign it with gpg and
attach it to the release as $source_sigfile.
!!!!!!!!!!!!!!!!!
EOF
else
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig "$tarfile"
assets="$assets -a $source_sigfile"
fi
fi
hubflags=''
if [ $prerelease -eq 1 ]; then
@@ -202,7 +245,7 @@ release_text=`mktemp`
echo "$tag" > "${release_text}"
echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}"
hub release create $hubflags $assets -f "${release_text}" "$tag"
hub release create $hubflags $assets -F "${release_text}" "$tag"
if [ $dodist -eq 0 ]; then
rm -rf "$builddir"
@@ -210,6 +253,9 @@ fi
rm "${release_text}"
rm "${latest_changes}"
# publish to npmjs
npm publish
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
npm run gendoc
@@ -225,11 +271,17 @@ if [ -z "$skip_jsdoc" ]; then
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
fi
# if it is a pre-release, leave it on the release branch for now.
if [ $prerelease -eq 1 ]; then
git checkout "$rel_branch"
exit 0
fi
# merge release branch to master
echo "updating master branch"
git checkout master
git pull
git merge --ff-only "$rel_branch"
git merge "$rel_branch"
# push master and docs (if generated) to github
git push origin master
@@ -237,9 +289,6 @@ if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
fi
# publish to npmjs
npm publish
# finally, merge master back onto develop
git checkout develop
git pull
+1 -1
View File
@@ -1,5 +1,5 @@
module.exports = {
env: {
jasmine: true,
mocha: true,
},
}
+223
View File
@@ -0,0 +1,223 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 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.
*/
"use strict";
// load olm before the sdk if possible
import './olm-loader';
import sdk from '..';
import testUtils from './test-utils';
import MockHttpBackend from 'matrix-mock-request';
import expect from 'expect';
import Promise from 'bluebird';
import LocalStorageCryptoStore from '../lib/crypto/store/localStorage-crypto-store';
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
*
* @constructor
* @param {string} userId
* @param {string} deviceId
* @param {string} accessToken
*
* @param {WebStorage=} sessionStoreBackend a web storage object to use for the
* session store. If undefined, we will create a MockStorageApi.
*/
export default function TestClient(
userId, deviceId, accessToken, sessionStoreBackend,
) {
this.userId = userId;
this.deviceId = deviceId;
if (sessionStoreBackend === undefined) {
sessionStoreBackend = new testUtils.MockStorageApi();
}
const sessionStore = new sdk.WebStorageSessionStore(sessionStoreBackend);
// expose this so the tests can get to it
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
this.httpBackend = new MockHttpBackend();
this.client = sdk.createClient({
baseUrl: "http://" + userId + ".test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
sessionStore: sessionStore,
cryptoStore: this.cryptoStore,
request: this.httpBackend.requestFn,
});
this.deviceKeys = null;
this.oneTimeKeys = {};
}
TestClient.prototype.toString = function() {
return 'TestClient[' + this.userId + ']';
};
/**
* start the client, and wait for it to initialise.
*
* @return {Promise}
*/
TestClient.prototype.start = function() {
console.log(this + ': starting');
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload();
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
this.httpBackend.when("GET", "/sync").respond(200, { next_batch: 1 });
this.client.startClient({
// set this so that we can get hold of failed events
pendingEventOrdering: 'detached',
});
return Promise.all([
this.httpBackend.flushAllExpected(),
testUtils.syncPromise(this.client),
]).then(() => {
console.log(this + ': started');
});
};
/**
* stop the client
*/
TestClient.prototype.stop = function() {
this.client.stopClient();
};
/**
* Set up expectations that the client will upload device keys.
*/
TestClient.prototype.expectDeviceKeyUpload = function() {
const self = this;
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
expect(content.one_time_keys).toBe(undefined);
expect(content.device_keys).toBeTruthy();
console.log(self + ': received device keys');
// we expect this to happen before any one-time keys are uploaded.
expect(Object.keys(self.oneTimeKeys).length).toEqual(0);
self.deviceKeys = content.device_keys;
return {one_time_key_counts: {signed_curve25519: 0}};
});
};
/**
* If one-time keys have already been uploaded, return them. Otherwise,
* set up an expectation that the keys will be uploaded, and wait for
* that to happen.
*
* @returns {Promise} for the one-time keys
*/
TestClient.prototype.awaitOneTimeKeyUpload = function() {
if (Object.keys(this.oneTimeKeys).length != 0) {
// already got one-time keys
return Promise.resolve(this.oneTimeKeys);
}
this.httpBackend.when("POST", "/keys/upload")
.respond(200, (path, content) => {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBe(undefined);
return {one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
}};
});
this.httpBackend.when("POST", "/keys/upload")
.respond(200, (path, content) => {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).toNotEqual({});
console.log('%s: received %i one-time keys', this,
Object.keys(content.one_time_keys).length);
this.oneTimeKeys = content.one_time_keys;
return {one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
}};
});
// this can take ages
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
expect(flushed).toEqual(2);
return this.oneTimeKeys;
});
};
/**
* Set up expectations that the client will query device keys.
*
* We check that the query contains each of the users in `response`.
*
* @param {Object} response response to the query.
*/
TestClient.prototype.expectKeyQuery = function(response) {
this.httpBackend.when('POST', '/keys/query').respond(
200, (path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys[userId]).toEqual({});
});
return response;
});
};
/**
* get the uploaded curve25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getDeviceKey = function() {
const keyId = 'curve25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
};
/**
* get the uploaded ed25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getSigningKey = function() {
const keyId = 'ed25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
};
/**
* flush a single /sync request, and wait for the syncing event
*
* @returns {Promise} promise which completes once the sync has been flushed
*/
TestClient.prototype.flushSync = function() {
console.log(`${this}: flushSync`);
return Promise.all([
this.httpBackend.flush('/sync', 1),
testUtils.syncPromise(this.client),
]).then(() => {
console.log(`${this}: flushSync completed`);
});
};
+400
View File
@@ -0,0 +1,400 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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 expect from 'expect';
import Promise from 'bluebird';
import TestClient from '../TestClient';
import testUtils from '../test-utils';
const ROOM_ID = "!room:id";
/**
* get a /sync response which contains a single e2e room (ROOM_ID), with the
* members given
*
* @param {string[]} roomMembers
*
* @return {object} sync response
*/
function getSyncResponse(roomMembers) {
const stateEvents = [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
}),
];
Array.prototype.push.apply(
stateEvents,
roomMembers.map(
(m) => testUtils.mkMembership({
mship: 'join',
sender: m,
}),
),
);
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[ROOM_ID]: {
state: {
events: stateEvents,
},
},
},
},
};
return syncResponse;
}
describe("DeviceList management:", function() {
if (!global.Olm) {
console.warn('not running deviceList tests: Olm not present');
return;
}
let sessionStoreBackend;
let aliceTestClient;
async function createTestClient() {
const testClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend,
);
await testClient.client.initCrypto();
return testClient;
}
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
// we create our own sessionStoreBackend so that we can use it for
// another TestClient.
sessionStoreBackend = new testUtils.MockStorageApi();
aliceTestClient = await createTestClient();
});
afterEach(function() {
aliceTestClient.stop();
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
console.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {},
},
});
return Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, {
event_id: '$event_id',
});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
});
});
it("We should not get confused by out-of-order device query responses",
() => {
// https://github.com/vector-im/riot-web/issues/3126
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
return aliceTestClient.flushSync();
}).then(() => {
// to make sure the initial device queries are flushed out, we
// attempt to send a message.
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
'@chris:abc': {},
},
},
);
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
200, {event_id: '$event1'});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1),
),
aliceTestClient.client._crypto._deviceList.saveIfDirty(),
]);
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
expect(data.syncToken).toEqual(1);
});
// invalidate bob's and chris's device lists in separate syncs
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '2',
device_lists: {
changed: ['@bob:xyz'],
},
});
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '3',
device_lists: {
changed: ['@chris:abc'],
},
});
// flush both syncs
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
}).then(() => {
// check that we don't yet have a request for chris's devices.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@chris:abc': {},
},
token: '3',
}).respond(200, {
device_keys: {'@chris:abc': {}},
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(0);
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
if (bobStat != 1 && bobStat != 2) {
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
bobStat);
}
const chrisStat = data.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
);
}
});
// now add an expectation for a query for bob's devices, and let
// it complete.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@bob:xyz': {},
},
token: '2',
}).respond(200, {
device_keys: {'@bob:xyz': {}},
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => {
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3);
const chrisStat = data.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
);
}
});
// now let the query for chris's devices complete.
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => {
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
const chrisStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(data.syncToken).toEqual(3);
});
});
}).timeout(3000);
// https://github.com/vector-im/riot-web/issues/4983
describe("Alice should know she has stale device lists", () => {
beforeEach(async function() {
await aliceTestClient.start();
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz']));
await aliceTestClient.flushSync();
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
},
},
);
await aliceTestClient.httpBackend.flush('/keys/query', 1);
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toBeGreaterThan(
0, "Alice should be tracking bob's device list",
);
});
});
it("when Bob leaves", async function() {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, {
next_batch: 2,
device_lists: {
left: ['@bob:xyz'],
},
rooms: {
join: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: 'leave',
sender: '@bob:xyz',
}),
],
},
},
},
},
},
);
await aliceTestClient.flushSync();
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
});
});
it("when Alice leaves", async function() {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, {
next_batch: 2,
device_lists: {
left: ['@bob:xyz'],
},
rooms: {
leave: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: 'leave',
sender: '@bob:xyz',
}),
],
},
},
},
},
},
);
await aliceTestClient.flushSync();
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
});
});
it("when Bob leaves whilst Alice is offline", async function() {
aliceTestClient.stop();
const anotherTestClient = await createTestClient();
try {
await anotherTestClient.start();
anotherTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse([]));
await anotherTestClient.flushSync();
await anotherTestClient.client._crypto._deviceList.saveIfDirty();
anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
});
} finally {
anotherTestClient.stop();
}
});
});
});
File diff suppressed because it is too large Load Diff
+34 -20
View File
@@ -1,8 +1,12 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import expect from 'expect';
import Promise from 'bluebird';
describe("MatrixClient events", function() {
const baseUrl = "http://localhost.or.something";
let client;
@@ -98,7 +102,7 @@ describe("MatrixClient events", function() {
};
it("should emit events from both the first and subsequent /sync calls",
function(done) {
function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
@@ -127,11 +131,16 @@ describe("MatrixClient events", function() {
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
// wait for two SYNCING events
utils.syncPromise(client).then(() => {
return utils.syncPromise(client);
}),
httpBackend.flushAllExpected(),
]).then(() => {
expect(expectedEvents.length).toEqual(
0, "Failed to see all events from /sync calls",
);
done();
});
});
@@ -141,8 +150,8 @@ describe("MatrixClient events", function() {
let fired = false;
client.on("User.presence", function(event, user) {
fired = true;
expect(user).toBeDefined();
expect(event).toBeDefined();
expect(user).toBeTruthy();
expect(event).toBeTruthy();
if (!user || !event) {
return;
}
@@ -154,13 +163,13 @@ describe("MatrixClient events", function() {
});
client.startClient();
httpBackend.flush().done(function() {
httpBackend.flushAllExpected().done(function() {
expect(fired).toBe(true, "User.presence didn't fire.");
done();
});
});
it("should emit Room events", function(done) {
it("should emit Room events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let roomInvokeCount = 0;
@@ -180,7 +189,10 @@ describe("MatrixClient events", function() {
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
]).then(function() {
expect(roomInvokeCount).toEqual(
1, "Room fired wrong number of times.",
);
@@ -190,11 +202,10 @@ describe("MatrixClient events", function() {
expect(timelineFireCount).toEqual(
3, "Room.timeline fired the wrong number of times",
);
done();
});
});
it("should emit RoomState events", function(done) {
it("should emit RoomState events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
@@ -207,7 +218,7 @@ describe("MatrixClient events", function() {
client.on("RoomState.events", function(event, state) {
eventsInvokeCount++;
const index = roomStateEventTypes.indexOf(event.getType());
expect(index).not.toEqual(
expect(index).toNotEqual(
-1, "Unexpected room state event type: " + event.getType(),
);
if (index >= 0) {
@@ -229,7 +240,10 @@ describe("MatrixClient events", function() {
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
]).then(function() {
expect(membersInvokeCount).toEqual(
1, "RoomState.members fired wrong number of times",
);
@@ -239,11 +253,10 @@ describe("MatrixClient events", function() {
expect(eventsInvokeCount).toEqual(
2, "RoomState.events fired wrong number of times",
);
done();
});
});
it("should emit RoomMember events", function(done) {
it("should emit RoomMember events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
@@ -268,7 +281,10 @@ describe("MatrixClient events", function() {
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
]).then(function() {
expect(typingInvokeCount).toEqual(
1, "RoomMember.typing fired wrong number of times",
);
@@ -281,11 +297,10 @@ describe("MatrixClient events", function() {
expect(membershipInvokeCount).toEqual(
1, "RoomMember.membership fired wrong number of times",
);
done();
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function(done) {
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' });
let sessionLoggedOutCount = 0;
@@ -295,11 +310,10 @@ describe("MatrixClient events", function() {
client.startClient();
httpBackend.flush().done(function() {
return httpBackend.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times",
);
done();
});
});
});
+200 -170
View File
@@ -1,7 +1,8 @@
"use strict";
const q = require("q");
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
const EventTimeline = sdk.EventTimeline;
@@ -81,7 +82,7 @@ function startClient(httpBackend, client) {
client.startClient();
// set up a promise which will resolve once the client is initialised
const deferred = q.defer();
const deferred = Promise.defer();
client.on("sync", function(state) {
console.log("sync", state);
if (state != "SYNCING") {
@@ -90,11 +91,12 @@ function startClient(httpBackend, client) {
deferred.resolve();
});
httpBackend.flush();
return deferred.promise;
return Promise.all([
httpBackend.flushAllExpected(),
deferred.promise,
]);
}
describe("getEventTimeline support", function() {
let httpBackend;
let client;
@@ -125,10 +127,10 @@ describe("getEventTimeline support", function() {
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).toThrow();
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("timeline support works when enabled", function(done) {
it("timeline support works when enabled", function() {
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
@@ -136,16 +138,13 @@ describe("getEventTimeline support", function() {
timelineSupport: true,
});
startClient(httpBackend, client,
).then(function() {
return startClient(httpBackend, client).then(() => {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).not.toThrow();
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
}).toNotThrow();
});
});
@@ -196,18 +195,19 @@ describe("getEventTimeline support", function() {
},
});
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 2),
]);
}).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(EVENTS[1]);
httpBackend.when("GET", "/messages").respond(200, {
chunk: [EVENTS[0]],
start: "pagin_start",
end: "pagin_end",
});
return httpBackend.flush("/sync", 2);
}).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(EVENTS[1]);
httpBackend.flush("/messages", 1);
return client.scrollback(room);
}).then(function() {
@@ -215,15 +215,17 @@ describe("getEventTimeline support", function() {
expect(room.timeline[0].event).toEqual(EVENTS[0]);
expect(room.timeline[1].event).toEqual(EVENTS[1]);
expect(room.oldState.paginationToken).toEqual("pagin_end");
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
});
import expect from 'expect';
describe("MatrixClient event timelines", function() {
let client = null;
let httpBackend = null;
beforeEach(function(done) {
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
@@ -235,8 +237,7 @@ describe("MatrixClient event timelines", function() {
timelineSupport: true,
});
startClient(httpBackend, client)
.catch(utils.failTest).done(done);
return startClient(httpBackend, client);
});
afterEach(function() {
@@ -245,7 +246,7 @@ describe("MatrixClient event timelines", function() {
});
describe("getEventTimeline", function() {
it("should create a new timeline for new events", function(done) {
it("should create a new timeline for new events", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
@@ -263,22 +264,23 @@ describe("MatrixClient event timelines", function() {
};
});
client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
for (let i = 0; i < 4; i++) {
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
expect(tl.getEvents()[i].sender.name).toEqual(userName);
}
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
return Promise.all([
client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
for (let i = 0; i < 4; i++) {
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
expect(tl.getEvents()[i].sender.name).toEqual(userName);
}
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token");
}),
httpBackend.flushAllExpected(),
]);
});
it("should return existing timeline for known events", function(done) {
it("should return existing timeline for known events", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
@@ -297,20 +299,23 @@ describe("MatrixClient event timelines", function() {
},
});
httpBackend.flush("/sync").then(function() {
return Promise.all([
httpBackend.flush("/sync"),
utils.syncPromise(client),
]).then(function() {
return client.getEventTimeline(timelineSet, EVENTS[0].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].sender.name).toEqual(userName);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("f_1_1");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS)).toEqual("s_5_4");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("f_1_1");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
// .toEqual("s_5_4");
});
});
it("should update timelines where they overlap a previous /sync", function(done) {
it("should update timelines where they overlap a previous /sync", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
@@ -342,6 +347,7 @@ describe("MatrixClient event timelines", function() {
};
});
const deferred = Promise.defer();
client.on("sync", function() {
client.getEventTimeline(timelineSet, EVENTS[2].event_id,
).then(function(tl) {
@@ -353,14 +359,18 @@ describe("MatrixClient event timelines", function() {
.toEqual("start_token");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
// .toEqual("s_5_4");
}).catch(utils.failTest).done(done);
}).done(() => deferred.resolve(),
(e) => deferred.reject(e));
});
httpBackend.flush().catch(utils.failTest);
return Promise.all([
httpBackend.flushAllExpected(),
deferred.promise,
]);
});
it("should join timelines where they overlap a previous /context",
function(done) {
function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
@@ -420,41 +430,42 @@ describe("MatrixClient event timelines", function() {
let tl0;
let tl3;
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl0 = tl;
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl3 = tl;
return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
}).then(function(tl) {
// we expect it to get merged in with event 2
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(tl0);
expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(tl3);
expect(tl0.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl0.getPaginationToken(EventTimeline.FORWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.BACKWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token3");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl0 = tl;
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl3 = tl;
return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
}).then(function(tl) {
// we expect it to get merged in with event 2
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(tl0);
expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(tl3);
expect(tl0.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl0.getPaginationToken(EventTimeline.FORWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.BACKWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token3");
}),
httpBackend.flushAllExpected(),
]);
});
it("should fail gracefully if there is no event field", function(done) {
it("should fail gracefully if there is no event field", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
@@ -470,20 +481,21 @@ describe("MatrixClient event timelines", function() {
};
});
client.getEventTimeline(timelineSet, "event1",
).then(function(tl) {
// could do with a fail()
expect(true).toBeFalsy();
}).catch(function(e) {
expect(String(e)).toMatch(/'event'/);
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
return Promise.all([
client.getEventTimeline(timelineSet, "event1",
).then(function(tl) {
// could do with a fail()
expect(true).toBeFalsy();
}, function(e) {
expect(String(e)).toMatch(/'event'/);
}),
httpBackend.flushAllExpected(),
]);
});
});
describe("paginateEventTimeline", function() {
it("should allow you to paginate backwards", function(done) {
it("should allow you to paginate backwards", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
@@ -514,27 +526,28 @@ describe("MatrixClient event timelines", function() {
});
let tl;
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(tl, {backwards: true});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token1");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token0");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(tl, {backwards: true});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token1");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token0");
}),
httpBackend.flushAllExpected(),
]);
});
it("should allow you to paginate forwards", function(done) {
it("should allow you to paginate forwards", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
@@ -565,24 +578,25 @@ describe("MatrixClient event timelines", function() {
});
let tl;
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(
tl, {backwards: false, limit: 20});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token1");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
return Promise.all([
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(
tl, {backwards: false, limit: 20});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token1");
}),
httpBackend.flushAllExpected(),
]);
});
});
@@ -617,56 +631,66 @@ describe("MatrixClient event timelines", function() {
});
});
it("should work when /send returns before /sync", function(done) {
it("should work when /send returns before /sync", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
// 2 because the initial sync contained an event
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
return Promise.all([
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
// 2 because the initial sync contained an event
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
// now let the sync complete, and check it again
return httpBackend.flush("/sync", 1);
}).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
}).catch(utils.failTest).done(done);
// now let the sync complete, and check it again
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
}).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
}),
httpBackend.flush("/send/m.room.message/" + TXN_ID, 1).catch(utils.failTest);
httpBackend.flush("/send/m.room.message/" + TXN_ID, 1),
]);
});
it("should work when /send returns after /sync", function(done) {
it("should work when /send returns after /sync", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
// initiate the send, and set up checks to be done when it completes
// - but note that it won't complete until after the /sync does, below.
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
console.log("sendTextMessage completed");
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (2)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
}).catch(utils.failTest).done(done);
return Promise.all([
// initiate the send, and set up checks to be done when it completes
// - but note that it won't complete until after the /sync does, below.
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
console.log("sendTextMessage completed");
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (2)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
}),
httpBackend.flush("/sync", 1).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (1)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (1)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
// now let the send complete.
return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1);
}).catch(utils.failTest);
// now let the send complete.
return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1);
}),
]);
});
});
@@ -703,7 +727,10 @@ describe("MatrixClient event timelines", function() {
};
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.flush().then(function() {
Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client),
]).then(function() {
const room = client.getRoom(roomId);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(3);
@@ -728,11 +755,14 @@ describe("MatrixClient event timelines", function() {
};
httpBackend.when("GET", "/sync").respond(200, sync2);
return httpBackend.flush();
return Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client),
]);
}).then(function() {
const room = client.getRoom(roomId);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
});
+33 -21
View File
@@ -1,6 +1,7 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const HttpBackend = require("matrix-mock-request");
const publicGlobals = require("../../lib/matrix");
const Room = publicGlobals.Room;
const MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
@@ -8,6 +9,8 @@ const Filter = publicGlobals.Filter;
const utils = require("../test-utils");
const MockStorageApi = require("../MockStorageApi");
import expect from 'expect';
describe("MatrixClient", function() {
const baseUrl = "http://localhost.or.something";
let client = null;
@@ -46,13 +49,16 @@ describe("MatrixClient", function() {
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.data).toEqual(buf);
expect(req.rawData).toEqual(buf);
expect(req.queryParams.filename).toEqual("hi.txt");
expect(req.queryParams.access_token).toEqual(accessToken);
if (!(req.queryParams.access_token == accessToken ||
req.headers["Authorization"] == "Bearer " + accessToken)) {
expect(true).toBe(false);
}
expect(req.headers["Content-Type"]).toEqual("text/plain");
expect(req.opts.json).toBeFalsy();
expect(req.opts.timeout).toBe(undefined);
}).respond(200, "content");
}).respond(200, "content", true);
const prom = client.uploadContent({
stream: buf,
@@ -60,7 +66,7 @@ describe("MatrixClient", function() {
type: "text/plain",
});
expect(prom).toBeDefined();
expect(prom).toBeTruthy();
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(1);
@@ -73,7 +79,7 @@ describe("MatrixClient", function() {
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(0);
}).catch(utils.failTest).done(done);
}).nodeify(done);
httpBackend.flush();
});
@@ -83,7 +89,7 @@ describe("MatrixClient", function() {
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.opts.json).toBeFalsy();
}).respond(200, JSON.stringify({ "content_uri": "uri" }));
}).respond(200, { "content_uri": "uri" });
client.uploadContent({
stream: buf,
@@ -93,22 +99,21 @@ describe("MatrixClient", function() {
rawResponse: false,
}).then(function(response) {
expect(response.content_uri).toEqual("uri");
}).catch(utils.failTest).done(done);
}).nodeify(done);
httpBackend.flush();
});
it("should parse errors into a MatrixError", function(done) {
// opts.json is false, so request returns unparsed json.
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.data).toEqual(buf);
expect(req.rawData).toEqual(buf);
expect(req.opts.json).toBeFalsy();
}).respond(400, JSON.stringify({
}).respond(400, {
"errcode": "M_SNAFU",
"error": "broken",
}));
});
client.uploadContent({
stream: buf,
@@ -120,7 +125,7 @@ describe("MatrixClient", function() {
expect(error.httpStatus).toEqual(400);
expect(error.errcode).toEqual("M_SNAFU");
expect(error.message).toEqual("broken");
}).catch(utils.failTest).done(done);
}).nodeify(done);
httpBackend.flush();
});
@@ -144,7 +149,7 @@ describe("MatrixClient", function() {
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(0);
}).catch(utils.failTest).done(done);
}).nodeify(done);
const r = client.cancelUpload(prom);
expect(r).toBe(true);
@@ -154,7 +159,7 @@ describe("MatrixClient", function() {
describe("joinRoom", function() {
it("should no-op if you've already joined a room", function() {
const roomId = "!foo:bar";
const room = new Room(roomId);
const room = new Room(roomId, userId);
room.addLiveEvents([
utils.mkMembership({
user: userId, room: roomId, mship: "join", event: true,
@@ -208,14 +213,14 @@ describe("MatrixClient", function() {
const httpFilterDefinition = {
event_format: "federation",
};
expect(store.getFilter(userId, filterId)).toBeNull();
expect(store.getFilter(userId, filterId)).toBe(null);
httpBackend.when(
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
).respond(200, httpFilterDefinition);
client.getFilter(userId, filterId, true).done(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
expect(store.getFilter(userId, filterId)).toBeDefined();
expect(store.getFilter(userId, filterId)).toBeTruthy();
done();
});
@@ -227,7 +232,7 @@ describe("MatrixClient", function() {
const filterId = "f1llllllerid";
it("should do an HTTP request and then store the filter", function(done) {
expect(store.getFilter(userId, filterId)).toBeNull();
expect(store.getFilter(userId, filterId)).toBe(null);
const filterDefinition = {
event_format: "client",
@@ -296,6 +301,14 @@ describe("MatrixClient", function() {
describe("downloadKeys", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
beforeEach(function() {
return client.initCrypto();
});
it("should do an HTTP request and then store the keys", function(done) {
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
// ed25519key = client.getDeviceEd25519Key();
@@ -348,7 +361,6 @@ describe("MatrixClient", function() {
httpBackend.when("POST", "/keys/query").check(function(req) {
expect(req.data).toEqual({device_keys: {
'@alice:localhost': {},
'boris': {},
'chaz': {},
}});
@@ -373,7 +385,7 @@ describe("MatrixClient", function() {
algorithms: ["2"],
unsigned: { "ghi": "def" },
});
}).catch(utils.failTest).done(done);
}).nodeify(done);
httpBackend.flush();
});
@@ -390,7 +402,7 @@ describe("MatrixClient", function() {
client.deleteDevice(
"my_device", auth,
).catch(utils.failTest).done(done);
).nodeify(done);
httpBackend.flush();
});
+17 -14
View File
@@ -1,9 +1,13 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
const HttpBackend = require("../mock-request");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import expect from 'expect';
import Promise from 'bluebird';
describe("MatrixClient opts", function() {
const baseUrl = "http://localhost.or.something";
let client = null;
@@ -90,13 +94,13 @@ describe("MatrixClient opts", function() {
httpBackend.flush("/txn1", 1);
});
it("should be able to sync / get new events", function(done) {
it("should be able to sync / get new events", async function() {
const expectedEventTypes = [ // from /initialSync
"m.room.message", "m.room.name", "m.room.member", "m.room.member",
"m.room.create",
];
client.on("event", function(event) {
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
expect(expectedEventTypes.indexOf(event.getType())).toNotEqual(
-1, "Recv unexpected event type: " + event.getType(),
);
expectedEventTypes.splice(
@@ -106,17 +110,16 @@ describe("MatrixClient opts", function() {
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush("/pushrules", 1).then(function() {
return httpBackend.flush("/filter", 1);
}).then(function() {
return httpBackend.flush("/sync", 1);
}).done(function() {
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes,
);
done();
});
await client.startClient();
await httpBackend.flush("/pushrules", 1);
await httpBackend.flush("/filter", 1);
await Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes,
);
});
});
+32 -9
View File
@@ -1,9 +1,14 @@
"use strict";
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
const EventStatus = sdk.EventStatus;
import expect from 'expect';
describe("MatrixClient retrying", function() {
const baseUrl = "http://localhost.or.something";
let client = null;
@@ -49,15 +54,20 @@ describe("MatrixClient retrying", function() {
});
it("should mark events as EventStatus.CANCELLED when cancelled", function(done) {
it("should mark events as EventStatus.CANCELLED when cancelled", function() {
// send a couple of events; the second will be queued
client.sendMessage(roomId, "m1").then(function(ev) {
expect(ev).toEqual(ev1);
});
client.sendMessage(roomId, "m2").then(function(ev) {
expect(ev).toEqual(ev2);
const p1 = client.sendMessage(roomId, "m1").then(function(ev) {
// we expect the first message to fail
throw new Error('Message 1 unexpectedly sent successfully');
}, (e) => {
// this is expected
});
// XXX: it turns out that the promise returned by this message
// never gets resolved.
// https://github.com/matrix-org/matrix-js-sdk/issues/496
client.sendMessage(roomId, "m2");
// both events should be in the timeline at this point
const tl = room.getLiveTimeline().getEvents();
expect(tl.length).toEqual(2);
@@ -83,7 +93,14 @@ describe("MatrixClient retrying", function() {
}).toThrow();
}).respond(400); // fail the first message
httpBackend.flush().then(function() {
// wait for the localecho of ev1 to be updated
const p3 = new Promise((resolve, reject) => {
room.on("Room.localEchoUpdated", (ev0) => {
if(ev0 === ev1) {
resolve();
}
});
}).then(function() {
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
expect(tl.length).toEqual(1);
@@ -91,7 +108,13 @@ describe("MatrixClient retrying", function() {
client.cancelPendingEvent(ev1);
expect(ev1.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(0);
}).catch(utils.failTest).done(done);
});
return Promise.all([
p1,
p3,
httpBackend.flushAllExpected(),
]);
});
describe("resending", function() {
+91 -64
View File
@@ -1,9 +1,13 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const EventStatus = sdk.EventStatus;
const HttpBackend = require("../mock-request");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import Promise from 'bluebird';
import expect from 'expect';
describe("MatrixClient room timelines", function() {
const baseUrl = "http://localhost.or.something";
let client = null;
@@ -120,7 +124,7 @@ describe("MatrixClient room timelines", function() {
client.startClient();
httpBackend.flush("/pushrules").then(function() {
return httpBackend.flush("/filter");
}).done(done);
}).nodeify(done);
});
afterEach(function() {
@@ -247,12 +251,15 @@ describe("MatrixClient room timelines", function() {
client.scrollback(room).done(function() {
expect(room.timeline.length).toEqual(1);
expect(room.oldState.paginationToken).toBeNull();
done();
expect(room.oldState.paginationToken).toBe(null);
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/sync", 1);
});
@@ -314,11 +321,14 @@ describe("MatrixClient room timelines", function() {
expect(oldMsg.sender.name).toEqual("Old Alice");
const newMsg = room.timeline[3];
expect(newMsg.sender.name).toEqual(userName);
done();
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/sync", 1);
});
@@ -345,11 +355,14 @@ describe("MatrixClient room timelines", function() {
expect(room.timeline.length).toEqual(3);
expect(room.timeline[0].event).toEqual(sbEvents[1]);
expect(room.timeline[1].event).toEqual(sbEvents[0]);
done();
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
done();
});
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/sync", 1);
});
@@ -367,15 +380,17 @@ describe("MatrixClient room timelines", function() {
return;
}
const room = client.getRoom(roomId);
expect(room.oldState.paginationToken).toBeDefined();
expect(room.oldState.paginationToken).toBeTruthy();
client.scrollback(room, 1).done(function() {
expect(room.oldState.paginationToken).toEqual(sbEndTok);
});
httpBackend.flush("/sync", 1);
httpBackend.flush("/messages", 1).done(function() {
done();
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
done();
});
});
});
httpBackend.flush("/sync", 1);
@@ -383,17 +398,17 @@ describe("MatrixClient room timelines", function() {
});
describe("new events", function() {
it("should be added to the right place in the timeline", function(done) {
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}),
];
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
let index = 0;
@@ -405,7 +420,10 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).then(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(index).toEqual(2);
expect(room.timeline.length).toEqual(3);
expect(room.timeline[2].event).toEqual(
@@ -414,12 +432,11 @@ describe("MatrixClient room timelines", function() {
expect(room.timeline[1].event).toEqual(
eventData[0],
);
}).catch(utils.failTest).done(done);
});
});
httpBackend.flush("/sync", 1);
});
it("should set the right event.sender values", function(done) {
it("should set the right event.sender values", function() {
const eventData = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMembership({
@@ -430,22 +447,24 @@ describe("MatrixClient room timelines", function() {
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).then(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
const preNameEvent = room.timeline[room.timeline.length - 3];
const postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender.name).toEqual(userName);
expect(postNameEvent.sender.name).toEqual("New Name");
}).catch(utils.failTest).done(done);
});
});
httpBackend.flush("/sync", 1);
});
it("should set the right room.name", function(done) {
it("should set the right room.name", function() {
const secondRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 2",
@@ -454,17 +473,20 @@ describe("MatrixClient room timelines", function() {
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
setNextSyncData([secondRoomNameEvent]);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
let nameEmitCount = 0;
client.on("Room.name", function(rm) {
nameEmitCount += 1;
});
httpBackend.flush("/sync", 1).done(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(nameEmitCount).toEqual(1);
expect(room.name).toEqual("Room 2");
// do another round
@@ -476,17 +498,18 @@ describe("MatrixClient room timelines", function() {
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
setNextSyncData([thirdRoomNameEvent]);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend.flush("/sync", 1).done(function() {
expect(nameEmitCount).toEqual(2);
expect(room.name).toEqual("Room 3");
done();
});
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
}).then(function() {
expect(nameEmitCount).toEqual(2);
expect(room.name).toEqual("Room 3");
});
});
httpBackend.flush("/sync", 1);
});
it("should set the right room members", function(done) {
it("should set the right room members", function() {
const userC = "@cee:bar";
const userD = "@dee:bar";
const eventData = [
@@ -501,12 +524,15 @@ describe("MatrixClient room timelines", function() {
eventData[1].__prev_event = null;
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).then(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC).name).toEqual("C");
expect(room.currentState.getMember(userC).membership).toEqual(
@@ -516,28 +542,30 @@ describe("MatrixClient room timelines", function() {
expect(room.currentState.getMember(userD).membership).toEqual(
"invite",
);
}).catch(utils.failTest).done(done);
});
});
httpBackend.flush("/sync", 1);
});
});
describe("gappy sync", function() {
it("should copy the last known state to the new timeline", function(done) {
it("should copy the last known state to the new timeline", function() {
const eventData = [
utils.mkMessage({user: userId, room: roomId}),
];
setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).done(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(eventData[0]);
expect(room.currentState.getMembers().length).toEqual(2);
@@ -549,23 +577,21 @@ describe("MatrixClient room timelines", function() {
expect(room.currentState.getMember(otherUserId).membership).toEqual(
"join",
);
done();
});
});
httpBackend.flush("/sync", 1);
});
it("should emit a 'Room.timelineReset' event", function(done) {
it("should emit a 'Room.timelineReset' event", function() {
const eventData = [
utils.mkMessage({user: userId, room: roomId}),
];
setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
let emitCount = 0;
@@ -575,12 +601,13 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).done(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(emitCount).toEqual(1);
done();
});
});
httpBackend.flush("/sync", 1);
});
});
});
+143 -66
View File
@@ -1,10 +1,14 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
const MatrixEvent = sdk.MatrixEvent;
const EventTimeline = sdk.EventTimeline;
import expect from 'expect';
import Promise from 'bluebird';
describe("MatrixClient syncing", function() {
const baseUrl = "http://localhost.or.something";
let client = null;
@@ -48,7 +52,7 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
httpBackend.flushAllExpected().done(function() {
done();
});
});
@@ -62,7 +66,7 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
httpBackend.flushAllExpected().done(function() {
done();
});
});
@@ -110,7 +114,7 @@ describe("MatrixClient syncing", function() {
};
});
it("should resolve incoming invites from /sync", function(done) {
it("should resolve incoming invites from /sync", function() {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC,
@@ -129,17 +133,20 @@ describe("MatrixClient syncing", function() {
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual("The Boss");
expect(
member.getAvatarUrl("home.server.url", null, null, null, false),
).toBeDefined();
done();
).toBeTruthy();
});
});
it("should use cached values from m.presence wherever possible", function(done) {
it("should use cached values from m.presence wherever possible", function() {
syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost",
@@ -157,14 +164,16 @@ describe("MatrixClient syncing", function() {
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual("The Ghost");
done();
});
});
it("should result in events on the room member firing", function(done) {
it("should result in events on the room member firing", function() {
syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost",
@@ -189,13 +198,15 @@ describe("MatrixClient syncing", function() {
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
expect(latestFiredName).toEqual("The Ghost");
done();
});
});
it("should no-op if resolveInvitesToProfiles is not set", function(done) {
it("should no-op if resolveInvitesToProfiles is not set", function() {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC,
@@ -206,13 +217,15 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual(userC);
expect(
member.getAvatarUrl("home.server.url", null, null, null, false),
).toBeNull();
done();
).toBe(null);
});
});
});
@@ -233,15 +246,17 @@ describe("MatrixClient syncing", function() {
};
it("should create users for presence events from /sync",
function(done) {
function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
expect(client.getUser(userA).presence).toEqual("online");
expect(client.getUser(userB).presence).toEqual("unavailable");
done();
});
});
});
@@ -352,65 +367,101 @@ describe("MatrixClient syncing", function() {
},
};
it("should continually recalculate the right room name.", function(done) {
it("should continually recalculate the right room name.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomOne);
// should have clobbered the name to the one from /events
expect(room.name).toEqual(
nextSyncData.rooms.join[roomOne].state.events[0].content.name,
);
done();
});
});
it("should store the right events in the timeline.", function(done) {
it("should store the right events in the timeline.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomTwo);
// should have added the message from /events
expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getContent().body).toEqual(msgText);
done();
});
});
it("should set the right room name.", function(done) {
it("should set the right room name.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomTwo);
// should use the display name of the other person.
expect(room.name).toEqual(otherDisplayName);
done();
});
});
it("should set the right user's typing flag.", function(done) {
it("should set the right user's typing flag.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomTwo);
let member = room.getMember(otherUserId);
expect(member).toBeDefined();
expect(member).toBeTruthy();
expect(member.typing).toEqual(true);
member = room.getMember(selfUserId);
expect(member).toBeDefined();
expect(member).toBeTruthy();
expect(member.typing).toEqual(false);
done();
});
});
// XXX: This test asserts that the js-sdk obeys the spec and treats state
// events that arrive in the incremental sync as if they preceeded the
// timeline events, however this breaks peeking, so it's disabled
// (see sync.js)
xit("should correctly interpret state in incremental sync.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomOne);
const stateAtStart = room.getLiveTimeline().getState(
EventTimeline.BACKWARDS,
);
const startRoomNameEvent = stateAtStart.getStateEvents('m.room.name', '');
expect(startRoomNameEvent.getContent().name).toEqual('Old room name');
const stateAtEnd = room.getLiveTimeline().getState(
EventTimeline.FORWARDS,
);
const endRoomNameEvent = stateAtEnd.getStateEvents('m.room.name', '');
expect(endRoomNameEvent.getContent().name).toEqual('A new room name');
});
});
@@ -445,10 +496,13 @@ describe("MatrixClient syncing", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]);
});
it("should set the back-pagination token on new rooms", function(done) {
it("should set the back-pagination token on new rooms", function() {
const syncData = {
next_batch: "batch_token",
rooms: {
@@ -468,16 +522,19 @@ describe("MatrixClient syncing", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.flush().then(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const room = client.getRoom(roomTwo);
expect(room).toExist();
const tok = room.getLiveTimeline()
.getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("roomtwotok");
done();
}).catch(utils.failTest).done();
});
});
it("should set the back-pagination token on gappy syncs", function(done) {
it("should set the back-pagination token on gappy syncs", function() {
const syncData = {
next_batch: "batch_token",
rooms: {
@@ -508,13 +565,15 @@ describe("MatrixClient syncing", function() {
expect(tok).toEqual("newerTok");
});
httpBackend.flush().then(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const room = client.getRoom(roomOne);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
expect(resetCallCount).toEqual(1);
done();
}).catch(utils.failTest).done();
});
});
});
@@ -567,7 +626,7 @@ describe("MatrixClient syncing", function() {
};
});
it("should sync receipts from /sync.", function(done) {
it("should sync receipts from /sync.", function() {
const ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
const receipt = {};
receipt[ackEvent.event_id] = {
@@ -585,7 +644,10 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const room = client.getRoom(roomOne);
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
type: "m.read",
@@ -594,7 +656,6 @@ describe("MatrixClient syncing", function() {
ts: 176592842636,
},
}]);
done();
});
});
});
@@ -614,7 +675,7 @@ describe("MatrixClient syncing", function() {
beforeEach(function(done) {
client.startClient();
httpBackend.flush().then(function() {
httpBackend.flushAllExpected().then(function() {
// the /sync call from syncLeftRooms ends up in the request
// queue behind the call from the running client; add a response
// to flush the client's one out.
@@ -624,29 +685,34 @@ describe("MatrixClient syncing", function() {
});
});
it("should create and use an appropriate filter", function(done) {
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 }});
}).respond(200, { filter_id: "another_id" });
const defer = Promise.defer();
httpBackend.when("GET", "/sync").check(function(req) {
expect(req.queryParams.filter).toEqual("another_id");
done();
defer.resolve();
}).respond(200, {});
client.syncLeftRooms();
// first flush the filter request; this will make syncLeftRooms
// make its /sync call
httpBackend.flush("/filter").then(function() {
// flush the syncs
return httpBackend.flush();
}).catch(utils.failTest);
return Promise.all([
httpBackend.flush("/filter").then(function() {
// flush the syncs
return httpBackend.flushAllExpected();
}),
defer.promise,
]);
});
it("should set the back-pagination token on left rooms", function(done) {
it("should set the back-pagination token on left rooms", function() {
const syncData = {
next_batch: "batch_token",
rooms: {
@@ -671,20 +737,31 @@ describe("MatrixClient syncing", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
client.syncLeftRooms().then(function() {
const room = client.getRoom(roomTwo);
const tok = room.getLiveTimeline().getPaginationToken(
EventTimeline.BACKWARDS);
return Promise.all([
client.syncLeftRooms().then(function() {
const room = client.getRoom(roomTwo);
const tok = room.getLiveTimeline().getPaginationToken(
EventTimeline.BACKWARDS);
expect(tok).toEqual("pagTok");
done();
}).catch(utils.failTest).done();
expect(tok).toEqual("pagTok");
}),
// first flush the filter request; this will make syncLeftRooms
// make its /sync call
httpBackend.flush("/filter").then(function() {
return httpBackend.flush();
}).catch(utils.failTest);
// first flush the filter request; this will make syncLeftRooms
// make its /sync call
httpBackend.flush("/filter").then(function() {
return httpBackend.flushAllExpected();
}),
]);
});
});
/**
* waits for the MatrixClient to emit one or more 'sync' events.
*
* @param {Number?} numSyncs number of syncs to wait for
* @returns {Promise} promise which resolves after the sync events have happened
*/
function awaitSyncEvent(numSyncs) {
return utils.syncPromise(client, numSyncs);
}
});
@@ -16,139 +16,34 @@ limitations under the License.
"use strict";
let Olm = null;
try {
Olm = require('olm');
} catch (e) {}
const anotherjson = require('another-json');
const q = require('q');
import Promise from 'bluebird';
import expect from 'expect';
const sdk = require('../..');
const utils = require('../../lib/utils');
const testUtils = require('../test-utils');
const MockHttpBackend = require('../mock-request');
const TestClient = require('../TestClient').default;
const ROOM_ID = "!room:id";
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
*
* @constructor
* @param {string} userId
* @param {string} deviceId
* @param {string} accessToken
*/
function TestClient(userId, deviceId, accessToken) {
this.userId = userId;
this.deviceId = deviceId;
this.storage = new sdk.WebStorageSessionStore(new testUtils.MockStorageApi());
this.httpBackend = new MockHttpBackend();
this.client = sdk.createClient({
baseUrl: "http://test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
sessionStore: this.storage,
request: this.httpBackend.requestFn,
});
this.deviceKeys = null;
this.oneTimeKeys = [];
}
/**
* start the client, and wait for it to initialise.
*
* @param {object?} existingDevices the list of our existing devices to return from
* the /query request. Defaults to empty device list
* @return {Promise}
*/
TestClient.prototype.start = function(existingDevices) {
const self = this;
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.httpBackend.when('POST', '/keys/query').respond(200, function(path, content) {
expect(content.device_keys[self.userId]).toEqual({});
let res = existingDevices;
if (!res) {
res = { device_keys: {} };
res.device_keys[self.userId] = {};
}
return res;
});
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
expect(content.one_time_keys).not.toBeDefined();
expect(content.device_keys).toBeDefined();
self.deviceKeys = content.device_keys;
return {one_time_key_counts: {signed_curve25519: 0}};
});
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
expect(content.device_keys).not.toBeDefined();
expect(content.one_time_keys).toBeDefined();
expect(content.one_time_keys).not.toEqual({});
self.oneTimeKeys = content.one_time_keys;
return {one_time_key_counts: {
signed_curve25519: utils.keys(self.oneTimeKeys).length,
}};
});
this.client.startClient({
// set this so that we can get hold of failed events
pendingEventOrdering: 'detached',
});
return this.httpBackend.flush();
};
/**
* stop the client
*/
TestClient.prototype.stop = function() {
this.client.stopClient();
};
/**
* get the uploaded curve25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getDeviceKey = function() {
const keyId = 'curve25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
};
/**
* get the uploaded ed25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getSigningKey = function() {
const keyId = 'ed25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
};
/**
* start an Olm session with a given recipient
*
* @param {Olm.Account} olmAccount
* @param {TestClient} recipientTestClient
* @return {Olm.Session}
* @return {Promise} promise for Olm.Session
*/
function createOlmSession(olmAccount, recipientTestClient) {
const otkId = utils.keys(recipientTestClient.oneTimeKeys)[0];
const otk = recipientTestClient.oneTimeKeys[otkId];
return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => {
const otkId = utils.keys(keys)[0];
const otk = keys[otkId];
const session = new Olm.Session();
session.create_outbound(
olmAccount, recipientTestClient.getDeviceKey(), otk.key,
);
return session;
const session = new global.Olm.Session();
session.create_outbound(
olmAccount, recipientTestClient.getDeviceKey(), otk.key,
);
return session;
});
}
/**
@@ -165,9 +60,9 @@ function createOlmSession(olmAccount, recipientTestClient) {
* @return {object} event
*/
function encryptOlmEvent(opts) {
expect(opts.senderKey).toBeDefined();
expect(opts.p2pSession).toBeDefined();
expect(opts.recipient).toBeDefined();
expect(opts.senderKey).toBeTruthy();
expect(opts.p2pSession).toBeTruthy();
expect(opts.recipient).toBeTruthy();
const plaintext = {
content: opts.plaincontent || {},
@@ -205,8 +100,8 @@ function encryptOlmEvent(opts) {
* @return {object} event
*/
function encryptMegolmEvent(opts) {
expect(opts.senderKey).toBeDefined();
expect(opts.groupSession).toBeDefined();
expect(opts.senderKey).toBeTruthy();
expect(opts.groupSession).toBeTruthy();
const plaintext = opts.plaintext || {};
if (!plaintext.content) {
@@ -219,11 +114,12 @@ function encryptMegolmEvent(opts) {
plaintext.type = "m.room.message";
}
if (!plaintext.room_id) {
expect(opts.room_id).toBeDefined();
expect(opts.room_id).toBeTruthy();
plaintext.room_id = opts.room_id;
}
return {
event_id: 'test_megolm_event',
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: opts.groupSession.encrypt(JSON.stringify(plaintext)),
@@ -306,9 +202,11 @@ function getSyncResponse(roomMembers) {
describe("megolm", function() {
if (!sdk.CRYPTO_ENABLED) {
if (!global.Olm) {
console.warn('not running megolm tests: Olm not present');
return;
}
const Olm = global.Olm;
let testOlmAccount;
let testSenderKey;
@@ -383,12 +281,13 @@ describe("megolm", function() {
return claimResponse;
}
beforeEach(function() {
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs",
);
await aliceTestClient.client.initCrypto();
testOlmAccount = new Olm.Account();
testOlmAccount.create();
@@ -400,10 +299,10 @@ describe("megolm", function() {
aliceTestClient.stop();
});
it("Alice receives a megolm message", function(done) {
return aliceTestClient.start().then(function() {
const p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
it("Alice receives a megolm message", function() {
return aliceTestClient.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
@@ -440,18 +339,96 @@ describe("megolm", function() {
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.httpBackend.flush("/sync", 1);
return aliceTestClient.flushSync();
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
expect(event.isEncrypted()).toBe(true);
return testUtils.awaitDecryption(event);
}).then((event) => {
expect(event.getContent().body).toEqual('42');
}).nodeify(done);
});
});
it("Alice gets a second room_key message", function(done) {
return aliceTestClient.start().then(function() {
const p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
it("Alice receives a megolm message before the session keys", function() {
// https://github.com/vector-im/riot-web/issues/2273
let roomKeyEncrypted;
return aliceTestClient.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event, but don't send it yet
roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
const messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// Alice just gets the message event to start with
const syncResponse = {
next_batch: 1,
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().msgtype).toEqual('m.bad.encrypted');
// now she gets the room_key event
const syncResponse = {
next_batch: 2,
to_device: {
events: [roomKeyEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
if (event.getContent().msgtype != 'm.bad.encrypted') {
return event;
}
return new Promise((resolve, reject) => {
event.once('Event.decrypted', (ev) => {
console.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
}).then((event) => {
expect(event.getContent().body).toEqual('42');
});
});
it("Alice gets a second room_key message", function() {
return aliceTestClient.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
@@ -508,22 +485,27 @@ describe("megolm", function() {
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse2);
return aliceTestClient.httpBackend.flush("/sync", 2);
// flush both syncs
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
}).nodeify(done);
});
});
it('Alice sends a megolm message', function(done) {
it('Alice sends a megolm message', function() {
let p2pSession;
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
return aliceTestClient.start().then(() => {
// establish an olm session with alice
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((_p2pSession) => {
p2pSession = _p2pSession;
const syncResponse = getSyncResponse(['@bob:xyz']);
const olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
@@ -534,20 +516,20 @@ describe("megolm", function() {
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
return aliceTestClient.flushSync();
}).then(function() {
// start out with the device unknown - the send should be rejected.
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
);
return q.all([
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
throw new Error("sendTextMessage failed on an unknown device");
}, (e) => {
expect(e.name).toEqual("UnknownDeviceError");
}),
aliceTestClient.httpBackend.flush(),
aliceTestClient.httpBackend.flushAllExpected(),
]);
}).then(function() {
// mark the device as known, and resend.
@@ -587,55 +569,23 @@ describe("megolm", function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const pendingMsg = room.getPendingEvents()[0];
return q.all([
return Promise.all([
aliceTestClient.client.resendEvent(pendingMsg, room),
aliceTestClient.httpBackend.flush(),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
}).nodeify(done);
});
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function(done) {
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
}).then(function() {
console.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {},
},
});
return q.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, {
event_id: '$event_id',
});
return q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
]);
}).nodeify(done);
});
it("We shouldn't attempt to send to blocked devices", function(done) {
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
it("We shouldn't attempt to send to blocked devices", function() {
return aliceTestClient.start().then(() => {
// establish an olm session with alice
const p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const syncResponse = getSyncResponse(['@bob:xyz']);
const olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
@@ -646,7 +596,7 @@ describe("megolm", function() {
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
return aliceTestClient.flushSync();
}).then(function() {
console.log('Forcing alice to download our device keys');
@@ -654,7 +604,7 @@ describe("megolm", function() {
200, getTestKeysQueryResponse('@bob:xyz'),
);
return q.all([
return Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
@@ -669,22 +619,28 @@ describe("megolm", function() {
event_id: '$event_id',
});
return q.all([
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
}).nodeify(done);
});
});
it("We should start a new megolm session when a device is blocked", function(done) {
it("We should start a new megolm session when a device is blocked", function() {
let p2pSession;
let megolmSessionId;
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
return aliceTestClient.start().then(() => {
// establish an olm session with alice
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((_p2pSession) => {
p2pSession = _p2pSession;
const syncResponse = getSyncResponse(['@bob:xyz']);
const olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
@@ -695,7 +651,7 @@ describe("megolm", function() {
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
return aliceTestClient.flushSync();
}).then(function() {
console.log("Fetching bob's devices and marking known");
@@ -703,9 +659,9 @@ describe("megolm", function() {
200, getTestKeysQueryResponse('@bob:xyz'),
);
return q.all([
return Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush(),
aliceTestClient.httpBackend.flushAllExpected(),
]).then((keys) => {
aliceTestClient.client.setDeviceKnown('@bob:xyz', 'DEVICE_ID');
});
@@ -736,9 +692,13 @@ describe("megolm", function() {
};
});
return q.all([
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
}).then(function() {
console.log('Telling alice to block our device');
@@ -749,21 +709,21 @@ describe("megolm", function() {
'PUT', '/send/',
).respond(200, function(path, content) {
console.log('/send:', content);
expect(content.session_id).not.toEqual(megolmSessionId);
expect(content.session_id).toNotEqual(megolmSessionId);
return {
event_id: '$event_id',
};
});
return q.all([
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
aliceTestClient.httpBackend.flush(),
aliceTestClient.httpBackend.flushAllExpected(),
]);
}).nodeify(done);
});
});
// https://github.com/vector-im/riot-web/issues/2676
it("Alice should send to her other devices", function(done) {
it("Alice should send to her other devices", function() {
// for this test, we make the testOlmAccount be another of Alice's devices.
// it ought to get included in messages Alice sends.
@@ -771,9 +731,7 @@ describe("megolm", function() {
let inboundGroupSession;
let decrypted;
return aliceTestClient.start(
getTestKeysQueryResponse(aliceTestClient.userId),
).then(function() {
return aliceTestClient.start().then(function() {
// an encrypted room with just alice
const syncResponse = {
next_batch: 1,
@@ -800,8 +758,26 @@ describe("megolm", function() {
};
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush();
// the completion of the first initialsync hould make Alice
// invalidate the device cache for all members in e2e rooms (ie,
// herself), and do a key query.
aliceTestClient.expectKeyQuery(
getTestKeysQueryResponse(aliceTestClient.userId),
);
return aliceTestClient.httpBackend.flushAllExpected();
}).then(function() {
// start out with the device unknown - the send should be rejected.
return aliceTestClient.client.sendTextMessage(ROOM_ID, 'test').then(() => {
throw new Error("sendTextMessage failed on an unknown device");
}, (e) => {
expect(e.name).toEqual("UnknownDeviceError");
expect(Object.keys(e.devices)).toEqual([aliceTestClient.userId]);
expect(Object.keys(e.devices[aliceTestClient.userId])).
toEqual(['DEVICE_ID']);
});
}).then(function() {
// mark the device as known, and resend.
aliceTestClient.client.setDeviceKnown(aliceTestClient.userId, 'DEVICE_ID');
aliceTestClient.httpBackend.when('POST', '/keys/claim').respond(
200, function(path, content) {
@@ -841,60 +817,31 @@ describe("megolm", function() {
};
});
return q.all([
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
}).then(function() {
expect(decrypted.type).toEqual('m.room.message');
expect(decrypted.content.body).toEqual('test');
}).nodeify(done);
});
});
it('Alice should wait for device list to complete when sending a megolm message',
function(done) {
let p2pSession;
let inboundGroupSession;
function() {
let downloadPromise;
let sendPromise;
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/',
).respond(200, function(path, content) {
const m = content.messages['@bob:xyz'].DEVICE_ID;
const ct = m.ciphertext[testSenderKey];
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
expect(decrypted.type).toEqual('m.room_key');
inboundGroupSession = new Olm.InboundGroupSession();
inboundGroupSession.create(decrypted.content.session_key);
return {};
});
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, function(path, content) {
const ct = content.ciphertext;
const r = inboundGroupSession.decrypt(ct);
console.log('Decrypted received megolm message', r);
expect(r.message_index).toEqual(0);
const decrypted = JSON.parse(r.plaintext);
expect(decrypted.type).toEqual('m.room.message');
expect(decrypted.content.body).toEqual('test');
return {
event_id: '$event_id',
};
});
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
return aliceTestClient.start().then(() => {
// establish an olm session with alice
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const syncResponse = getSyncResponse(['@bob:xyz']);
const olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
@@ -905,11 +852,10 @@ describe("megolm", function() {
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
return aliceTestClient.flushSync();
}).then(function() {
console.log('Forcing alice to download our device keys');
// this will block
console.log('Forcing alice to download our device keys');
downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
// so will this.
@@ -924,21 +870,20 @@ describe("megolm", function() {
200, getTestKeysQueryResponse('@bob:xyz'),
);
return aliceTestClient.httpBackend.flush();
return aliceTestClient.httpBackend.flushAllExpected();
}).then(function() {
return q.all([downloadPromise, sendPromise]);
}).nodeify(done);
return Promise.all([downloadPromise, sendPromise]);
});
});
it("Alice exports megolm keys and imports them to a new device", function(done) {
it("Alice exports megolm keys and imports them to a new device", function() {
let messageEncrypted;
return aliceTestClient.start().then(() => {
const p2pSession = createOlmSession(
testOlmAccount, aliceTestClient,
);
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
@@ -975,7 +920,7 @@ describe("megolm", function() {
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.httpBackend.flush("/sync", 1);
return aliceTestClient.flushSync();
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
@@ -989,10 +934,10 @@ describe("megolm", function() {
aliceTestClient = new TestClient(
"@alice:localhost", "device2", "access_token2",
);
aliceTestClient.client.importRoomKeys(exported);
return aliceTestClient.start();
return aliceTestClient.client.initCrypto().then(() => {
aliceTestClient.client.importRoomKeys(exported);
return aliceTestClient.start();
});
}).then(function() {
const syncResponse = {
next_batch: 1,
@@ -1007,11 +952,11 @@ describe("megolm", function() {
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.httpBackend.flush("/sync", 1);
return aliceTestClient.flushSync();
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
}).nodeify(done);
});
});
});
-286
View File
@@ -1,286 +0,0 @@
"use strict";
const q = require("q");
/**
* Construct a mock HTTP backend, heavily inspired by Angular.js.
* @constructor
*/
function HttpBackend() {
this.requests = [];
this.expectedRequests = [];
const self = this;
// the request function dependency that the SDK needs.
this.requestFn = function(opts, callback) {
const req = new Request(opts, callback);
console.log("HTTP backend received request: %s", req);
self.requests.push(req);
const abort = function() {
const idx = self.requests.indexOf(req);
if (idx >= 0) {
console.log("Aborting HTTP request: %s %s", opts.method,
opts.uri);
self.requests.splice(idx, 1);
req.callback("aborted");
}
};
return {
abort: abort,
};
};
}
HttpBackend.prototype = {
/**
* Respond to all of the requests (flush the queue).
* @param {string} path The path to flush (optional) default: all.
* @param {integer} numToFlush The number of things to flush (optional), default: all.
* @return {Promise} resolved when there is nothing left to flush.
*/
flush: function(path, numToFlush) {
const defer = q.defer();
const self = this;
let flushed = 0;
let triedWaiting = false;
console.log(
"HTTP backend flushing... (path=%s numToFlush=%s)", path, numToFlush,
);
const tryFlush = function() {
// if there's more real requests and more expected requests, flush 'em.
console.log(
" trying to flush queue => reqs=%s expected=%s [%s]",
self.requests.length, self.expectedRequests.length, path,
);
if (self._takeFromQueue(path)) {
// try again on the next tick.
console.log(" flushed. Trying for more. [%s]", path);
flushed += 1;
if (numToFlush && flushed === numToFlush) {
console.log(" [%s] Flushed assigned amount: %s", path, numToFlush);
defer.resolve();
} else {
setTimeout(tryFlush, 0);
}
} else if (flushed === 0 && !triedWaiting) {
// we may not have made the request yet, wait a generous amount of
// time before giving up.
setTimeout(tryFlush, 5);
triedWaiting = true;
} else {
console.log(" no more flushes. [%s]", path);
defer.resolve();
}
};
setTimeout(tryFlush, 0);
return defer.promise;
},
/**
* Attempts to resolve requests/expected requests.
* @param {string} path The path to flush (optional) default: all.
* @return {boolean} true if something was resolved.
*/
_takeFromQueue: function(path) {
let req = null;
let i;
let j;
let matchingReq = null;
let expectedReq = null;
let testResponse = null;
for (i = 0; i < this.requests.length; i++) {
req = this.requests[i];
for (j = 0; j < this.expectedRequests.length; j++) {
expectedReq = this.expectedRequests[j];
if (path && path !== expectedReq.path) {
continue;
}
if (expectedReq.method === req.method &&
req.path.indexOf(expectedReq.path) !== -1) {
if (!expectedReq.data || (JSON.stringify(expectedReq.data) ===
JSON.stringify(req.data))) {
matchingReq = expectedReq;
this.expectedRequests.splice(j, 1);
break;
}
}
}
if (matchingReq) {
// remove from request queue
this.requests.splice(i, 1);
i--;
for (j = 0; j < matchingReq.checks.length; j++) {
matchingReq.checks[j](req);
}
testResponse = matchingReq.response;
console.log(" responding to %s", matchingReq.path);
let body = testResponse.body;
if (Object.prototype.toString.call(body) == "[object Function]") {
body = body(req.path, req.data);
}
req.callback(
testResponse.err, testResponse.response, body,
);
matchingReq = null;
}
}
if (testResponse) { // flushed something
return true;
}
return false;
},
/**
* Makes sure that the SDK hasn't sent any more requests to the backend.
*/
verifyNoOutstandingRequests: function() {
const firstOutstandingReq = this.requests[0] || {};
expect(this.requests.length).toEqual(0,
"Expected no more HTTP requests but received request to " +
firstOutstandingReq.path,
);
},
/**
* Makes sure that the test doesn't have any unresolved requests.
*/
verifyNoOutstandingExpectation: function() {
const firstOutstandingExpectation = this.expectedRequests[0] || {};
expect(this.expectedRequests.length).toEqual(0,
"Expected to see HTTP request for " + firstOutstandingExpectation.path,
);
},
/**
* Create an expected request.
* @param {string} method The HTTP method
* @param {string} path The path (which can be partial)
* @param {Object} data The expected data.
* @return {Request} An expected request.
*/
when: function(method, path, data) {
const pendingReq = new ExpectedRequest(method, path, data);
this.expectedRequests.push(pendingReq);
return pendingReq;
},
};
/**
* Represents the expectation of a request.
*
* <p>Includes the conditions to be matched against, the checks to be made,
* and the response to be returned.
*
* @constructor
* @param {string} method
* @param {string} path
* @param {object?} data
*/
function ExpectedRequest(method, path, data) {
this.method = method;
this.path = path;
this.data = data;
this.response = null;
this.checks = [];
}
ExpectedRequest.prototype = {
/**
* Execute a check when this request has been satisfied.
* @param {Function} fn The function to execute.
* @return {Request} for chaining calls.
*/
check: function(fn) {
this.checks.push(fn);
return this;
},
/**
* Respond with the given data when this request is satisfied.
* @param {Number} code The HTTP status code.
* @param {Object|Function} data The HTTP JSON body. If this is a function,
* it will be invoked when the JSON body is required (which should be returned).
*/
respond: function(code, data) {
this.response = {
response: {
statusCode: code,
headers: {},
},
body: data,
err: null,
};
},
/**
* Fail with an Error when this request is satisfied.
* @param {Number} code The HTTP status code.
* @param {Error} err The error to throw (e.g. Network Error)
*/
fail: function(code, err) {
this.response = {
response: {
statusCode: code,
headers: {},
},
body: null,
err: err,
};
},
};
/**
* Represents a request made by the app.
*
* @constructor
* @param {object} opts opts passed to request()
* @param {function} callback
*/
function Request(opts, callback) {
this.opts = opts;
this.callback = callback;
Object.defineProperty(this, 'method', {
get: function() {
return opts.method;
},
});
Object.defineProperty(this, 'path', {
get: function() {
return opts.uri;
},
});
Object.defineProperty(this, 'data', {
get: function() {
return opts.body;
},
});
Object.defineProperty(this, 'queryParams', {
get: function() {
return opts.qs;
},
});
Object.defineProperty(this, 'headers', {
get: function() {
return opts.headers || {};
},
});
}
Request.prototype = {
toString: function() {
return this.method + " " + this.path;
},
};
/**
* The HttpBackend class.
*/
module.exports = HttpBackend;
+24
View File
@@ -0,0 +1,24 @@
/*
Copyright 2017 Vector creations 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.
*/
// try to load the olm library.
try {
global.Olm = require('olm');
console.log('loaded libolm');
} catch (e) {
console.warn("unable to run crypto tests: libolm not available");
}
+68 -36
View File
@@ -1,14 +1,53 @@
"use strict";
const sdk = require("..");
import expect from 'expect';
import Promise from 'bluebird';
// load olm before the sdk if possible
import './olm-loader';
import sdk from '..';
const MatrixEvent = sdk.MatrixEvent;
/**
* Return a promise that is resolved when the client next emits a
* SYNCING event.
* @param {Object} client The client
* @param {Number=} count Number of syncs to wait for (default 1)
* @return {Promise} Resolves once the client has emitted a SYNCING event
*/
module.exports.syncPromise = function(client, count) {
if (count === undefined) {
count = 1;
}
if (count <= 0) {
return Promise.resolve();
}
const p = new Promise((resolve, reject) => {
const cb = (state) => {
console.log(`${Date.now()} syncPromise(${count}): ${state}`);
if (state == 'SYNCING') {
resolve();
} else {
client.once('sync', cb);
}
};
client.once('sync', cb);
});
return p.then(() => {
return module.exports.syncPromise(client, count-1);
});
};
/**
* Perform common actions before each test case, e.g. printing the test case
* name to stdout.
* @param {TestCase} testCase The test case that is about to be run.
* @param {Mocha.Context} context The test context
*/
module.exports.beforeEach = function(testCase) {
const desc = testCase.suite.description + " : " + testCase.description;
module.exports.beforeEach = function(context) {
const desc = context.currentTest.fullTitle();
console.log(desc);
console.log(new Array(1 + desc.length).join("="));
};
@@ -20,18 +59,18 @@ module.exports.beforeEach = function(testCase) {
* @return {Object} An instantiated object with spied methods/properties.
*/
module.exports.mock = function(constr, name) {
// By Tim Buschtöns
// Based on
// http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
const HelperConstr = new Function(); // jshint ignore:line
HelperConstr.prototype = constr.prototype;
const result = new HelperConstr();
result.jasmineToString = function() {
result.toString = function() {
return "mock" + (name ? " of " + name : "");
};
for (const key in constr.prototype) { // eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jasmine.createSpy((name || "mock") + '.' + key);
result[key] = expect.createSpy();
}
} catch (ex) {
// Direct access to some non-function fields of DOM prototypes may
@@ -155,35 +194,6 @@ module.exports.mkMessage = function(opts) {
};
/**
* make the test fail, with the given exception
*
* <p>This is useful for use with integration tests which use asyncronous
* methods: it can be added as a 'catch' handler in a promise chain.
*
* @param {Error} err exception to be reported
*
* @deprecated
* It turns out there are easier ways of doing this. Just use nodeify():
*
* it("should not throw", function(done) {
* asynchronousMethod().then(function() {
* // some tests
* }).nodeify(done);
* });
*
* @example
* it("should not throw", function(done) {
* asynchronousMethod().then(function() {
* // some tests
* }).catch(utils.failTest).done(done);
* });
*/
module.exports.failTest = function(err) {
expect(true).toBe(false, "Testfunc threw: " + err.stack);
};
/**
* A mock implementation of webstorage
*
@@ -209,3 +219,25 @@ module.exports.MockStorageApi.prototype = {
delete this.data[k];
},
};
/**
* If an event is being decrypted, wait for it to finish being decrypted.
*
* @param {MatrixEvent} event
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
*/
module.exports.awaitDecryption = function(event) {
if (!event.isBeingDecrypted()) {
return Promise.resolve(event);
}
console.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
return new Promise((resolve, reject) => {
event.once('Event.decrypted', (ev) => {
console.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
};
+3
View File
@@ -1,7 +1,10 @@
"use strict";
import 'source-map-support/register';
const ContentRepo = require("../../lib/content-repo");
const testUtils = require("../test-utils");
import expect from 'expect';
describe("ContentRepo", function() {
const baseUrl = "https://my.home.server";
+8 -2
View File
@@ -1,13 +1,19 @@
"use strict";
const Crypto = require("../../lib/crypto");
import 'source-map-support/register';
const sdk = require("../..");
let Crypto;
if (sdk.CRYPTO_ENABLED) {
Crypto = require("../../lib/crypto");
}
import expect from 'expect';
describe("Crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
it("Crypto exposes the correct olm library version", function() {
expect(Crypto.getOlmVersion()[0]).toEqual(2);
});
+149
View File
@@ -0,0 +1,149 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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 DeviceList from '../../../lib/crypto/DeviceList';
import MockStorageApi from '../../MockStorageApi';
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
import testUtils from '../../test-utils';
import utils from '../../../lib/utils';
import expect from 'expect';
import Promise from 'bluebird';
const signedDeviceList = {
"failures": {},
"device_keys": {
"@test1:sw1v.org": {
"HGKAWHRVJQ": {
"signatures": {
"@test1:sw1v.org": {
"ed25519:HGKAWHRVJQ":
"8PB450fxKDn5s8IiRZ2N2t6MiueQYVRLHFEzqIi1eLdxx1w" +
"XEPC1/1Uz9T4gwnKlMVAKkhB5hXQA/3kjaeLABw",
},
},
"user_id": "@test1:sw1v.org",
"keys": {
"ed25519:HGKAWHRVJQ":
"0gI/T6C+mn1pjtvnnW2yB2l1IIBb/5ULlBXi/LXFSEQ",
"curve25519:HGKAWHRVJQ":
"mbIZED1dBsgIgkgzxDpxKkJmsr4hiWlGzQTvUnQe3RY",
},
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2",
],
"device_id": "HGKAWHRVJQ",
"unsigned": {},
},
},
},
};
describe('DeviceList', function() {
let downloadSpy;
let sessionStore;
let cryptoStore;
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
downloadSpy = expect.createSpy();
const mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
cryptoStore = new MemoryCryptoStore();
});
function createTestDeviceList() {
const baseApis = {
downloadKeysForUsers: downloadSpy,
};
const mockOlm = {
verifySignature: function(key, message, signature) {},
};
return new DeviceList(baseApis, cryptoStore, sessionStore, mockOlm);
}
it("should successfully download and store device keys", function() {
const dl = createTestDeviceList();
dl.startTrackingDeviceList('@test1:sw1v.org');
const queryDefer1 = Promise.defer();
downloadSpy.andReturn(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
return prom1.then(() => {
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
});
});
it("should have an outdated devicelist on an invalidation while an " +
"update is in progress", function() {
const dl = createTestDeviceList();
dl.startTrackingDeviceList('@test1:sw1v.org');
const queryDefer1 = Promise.defer();
downloadSpy.andReturn(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
downloadSpy.reset();
// outdated notif arrives while the request is in flight.
const queryDefer2 = Promise.defer();
downloadSpy.andReturn(queryDefer2.promise);
dl.invalidateUserDeviceList('@test1:sw1v.org');
dl.refreshOutdatedDeviceLists();
dl.saveIfDirty().then(() => {
// the first request completes
queryDefer1.resolve({
device_keys: {
'@test1:sw1v.org': {},
},
});
return prom1;
}).then(() => {
// uh-oh; user restarts before second request completes. The new instance
// should know we never got a complete device list.
console.log("Creating new devicelist to simulate app reload");
downloadSpy.reset();
const dl2 = createTestDeviceList();
const queryDefer3 = Promise.defer();
downloadSpy.andReturn(queryDefer3.promise);
const prom3 = dl2.refreshOutdatedDeviceLists();
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
queryDefer3.resolve(utils.deepCopy(signedDeviceList));
// allow promise chain to complete
return prom3;
}).then(() => {
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
});
});
});
+270
View File
@@ -0,0 +1,270 @@
try {
global.Olm = require('olm');
} catch (e) {
console.warn("unable to run megolm tests: libolm not available");
}
import expect from 'expect';
import Promise from 'bluebird';
import sdk from '../../../..';
import algorithms from '../../../../lib/crypto/algorithms';
import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi';
import testUtils from '../../../test-utils';
// Crypto and OlmDevice won't import unless we have global.Olm
let OlmDevice;
let Crypto;
if (global.Olm) {
OlmDevice = require('../../../../lib/crypto/OlmDevice');
Crypto = require('../../../../lib/crypto');
}
const MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const ROOM_ID = '!ROOM:ID';
describe("MegolmDecryption", function() {
if (!global.Olm) {
console.warn('Not running megolm unit tests: libolm not present');
return;
}
let megolmDecryption;
let mockOlmLib;
let mockCrypto;
let mockBaseApis;
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockBaseApis = {};
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
});
// we stub out the olm encryption bits
mockOlmLib = {};
mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy();
mockOlmLib.encryptMessageForDevice =
expect.createSpy().andReturn(Promise.resolve());
megolmDecryption.olmlib = mockOlmLib;
});
describe('receives some keys:', function() {
let groupSession;
beforeEach(function() {
groupSession = new global.Olm.OutboundGroupSession();
groupSession.create();
// construct a fake decrypted key event via the use of a mocked
// 'crypto' implementation.
const event = new MatrixEvent({
type: 'm.room.encrypted',
});
const decryptedData = {
clearEvent: {
type: 'm.room_key',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
room_id: ROOM_ID,
session_id: groupSession.session_id(),
session_key: groupSession.session_key(),
},
},
senderCurve25519Key: "SENDER_CURVE25519",
claimedEd25519Key: "SENDER_ED25519",
};
const mockCrypto = {
decryptEvent: function() {
return Promise.resolve(decryptedData);
},
};
return event.attemptDecryption(mockCrypto).then(() => {
megolmDecryption.onRoomKeyEvent(event);
});
});
it('can decrypt an event', function() {
const event = new MatrixEvent({
type: 'm.room.encrypted',
room_id: ROOM_ID,
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: "SENDER_CURVE25519",
session_id: groupSession.session_id(),
ciphertext: groupSession.encrypt(JSON.stringify({
room_id: ROOM_ID,
content: 'testytest',
})),
},
});
return megolmDecryption.decryptEvent(event).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
});
});
it('can respond to a key request event', function() {
const keyRequest = {
userId: '@alice:foo',
deviceId: 'alidevice',
requestBody: {
room_id: ROOM_ID,
sender_key: "SENDER_CURVE25519",
session_id: groupSession.session_id(),
},
};
return megolmDecryption.hasKeysForKeyRequest(
keyRequest,
).then((hasKeys) => {
expect(hasKeys).toBe(true);
// set up some pre-conditions for the share call
const deviceInfo = {};
mockCrypto.getStoredDevice.andReturn(deviceInfo);
mockOlmLib.ensureOlmSessionsForDevices.andReturn(
Promise.resolve({'@alice:foo': {'alidevice': {
sessionId: 'alisession',
}}}),
);
const awaitEncryptForDevice = new Promise((res, rej) => {
mockOlmLib.encryptMessageForDevice.andCall(() => {
res();
return Promise.resolve();
});
});
mockBaseApis.sendToDevice = expect.createSpy();
// do the share
megolmDecryption.shareKeysWithDevice(keyRequest);
// it's asynchronous, so we have to wait a bit
return awaitEncryptForDevice;
}).then(() => {
// check that it called encryptMessageForDevice with
// appropriate args.
expect(mockOlmLib.encryptMessageForDevice.calls.length)
.toEqual(1);
const call = mockOlmLib.encryptMessageForDevice.calls[0];
const payload = call.arguments[6];
expect(payload.type).toEqual("m.forwarded_room_key");
expect(payload.content).toInclude({
sender_key: "SENDER_CURVE25519",
sender_claimed_ed25519_key: "SENDER_ED25519",
session_id: groupSession.session_id(),
chain_index: 0,
forwarding_curve25519_key_chain: [],
});
expect(payload.content.session_key).toExist();
});
});
it("can detect replay attacks", function() {
// trying to decrypt two different messages (marked by different
// event IDs or timestamps) using the same (sender key, session id,
// message index) triple should result in an exception being thrown
// as it should be detected as a replay attack.
const sessionId = groupSession.session_id();
const cipherText = groupSession.encrypt(JSON.stringify({
room_id: ROOM_ID,
content: 'testytest',
}));
const event1 = new MatrixEvent({
type: 'm.room.encrypted',
room_id: ROOM_ID,
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: "SENDER_CURVE25519",
session_id: sessionId,
ciphertext: cipherText,
},
event_id: "$event1",
origin_server_ts: 1507753886000,
});
const successHandler = expect.createSpy();
const failureHandler = expect.createSpy()
.andCall((err) => {
expect(err.toString()).toMatch(
/Duplicate message index, possible replay attack/,
);
});
return megolmDecryption.decryptEvent(event1).then((res) => {
const event2 = new MatrixEvent({
type: 'm.room.encrypted',
room_id: ROOM_ID,
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: "SENDER_CURVE25519",
session_id: sessionId,
ciphertext: cipherText,
},
event_id: "$event2",
origin_server_ts: 1507754149000,
});
return megolmDecryption.decryptEvent(event2);
}).then(
successHandler,
failureHandler,
).then(() => {
expect(successHandler).toNotHaveBeenCalled();
expect(failureHandler).toHaveBeenCalled();
});
});
it("allows re-decryption of the same event", function() {
// in contrast with the previous test, if the event ID and
// timestamp are the same, then it should not be considered a
// replay attack
const sessionId = groupSession.session_id();
const cipherText = groupSession.encrypt(JSON.stringify({
room_id: ROOM_ID,
content: 'testytest',
}));
const event = new MatrixEvent({
type: 'm.room.encrypted',
room_id: ROOM_ID,
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: "SENDER_CURVE25519",
session_id: sessionId,
ciphertext: cipherText,
},
event_id: "$event1",
origin_server_ts: 1507753886000,
});
return megolmDecryption.decryptEvent(event).then((res) => {
return megolmDecryption.decryptEvent(event);
// test is successful if no exception is thrown
});
});
});
});
+12 -10
View File
@@ -1,4 +1,5 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const EventTimeline = sdk.EventTimeline;
const utils = require("../test-utils");
@@ -8,6 +9,8 @@ function mockRoomStates(timeline) {
timeline._endState = utils.mock(sdk.RoomState, "endState");
}
import expect from 'expect';
describe("EventTimeline", function() {
const roomId = "!foo:bar";
const userA = "@alice:bar";
@@ -75,7 +78,7 @@ describe("EventTimeline", function() {
expect(function() {
timeline.initialiseState(state);
}).not.toThrow();
}).toNotThrow();
timeline.addEvent(event, false);
expect(function() {
timeline.initialiseState(state);
@@ -118,7 +121,7 @@ describe("EventTimeline", function() {
const next = {b: "b"};
expect(function() {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).not.toThrow();
}).toNotThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(prev);
expect(function() {
@@ -127,7 +130,7 @@ describe("EventTimeline", function() {
expect(function() {
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
}).not.toThrow();
}).toNotThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(next);
expect(function() {
@@ -184,14 +187,14 @@ describe("EventTimeline", function() {
name: "Old Alice",
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
.andCallFake(function(uid) {
.andCall(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
.andCallFake(function(uid) {
.andCall(function(uid) {
if (uid === userA) {
return oldSentinel;
}
@@ -226,14 +229,14 @@ describe("EventTimeline", function() {
name: "Old Alice",
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
.andCallFake(function(uid) {
.andCall(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
.andCallFake(function(uid) {
.andCall(function(uid) {
if (uid === userA) {
return oldSentinel;
}
@@ -278,7 +281,7 @@ describe("EventTimeline", function() {
expect(events[1].forwardLooking).toBe(true);
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
not.toHaveBeenCalled();
toNotHaveBeenCalled();
});
@@ -308,7 +311,7 @@ describe("EventTimeline", function() {
expect(events[1].forwardLooking).toBe(false);
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
not.toHaveBeenCalled();
toNotHaveBeenCalled();
});
});
@@ -373,4 +376,3 @@ describe("EventTimeline", function() {
});
});
});
+82
View File
@@ -0,0 +1,82 @@
/*
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 sdk from '../..';
const MatrixEvent = sdk.MatrixEvent;
import testUtils from '../test-utils';
import expect from 'expect';
import Promise from 'bluebird';
describe("MatrixEvent", () => {
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe(".attemptDecryption", () => {
let encryptedEvent;
beforeEach(() => {
encryptedEvent = new MatrixEvent({
id: 'test_encrypted_event',
type: 'm.room.encrypted',
content: {
ciphertext: 'secrets',
},
});
});
it('should retry decryption if a retry is queued', () => {
let callCount = 0;
let prom2;
const crypto = {
decryptEvent: function() {
++callCount;
console.log(`decrypt: ${callCount}`);
if (callCount == 1) {
// schedule a second decryption attempt while
// the first one is still running.
prom2 = encryptedEvent.attemptDecryption(crypto);
const error = new Error("nope");
error.name = 'DecryptionError';
return Promise.reject(error);
} else {
expect(prom2.isFulfilled()).toBe(
false, 'second attemptDecryption resolved too soon');
return Promise.resolve({
clearEvent: {
type: 'm.room.message',
},
});
}
},
};
return encryptedEvent.attemptDecryption(crypto).then(() => {
expect(callCount).toEqual(2);
expect(encryptedEvent.getType()).toEqual('m.room.message');
// make sure the second attemptDecryption resolves
return prom2;
});
});
});
});
+3
View File
@@ -1,8 +1,11 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const Filter = sdk.Filter;
const utils = require("../test-utils");
import expect from 'expect';
describe("Filter", function() {
const filterId = "f1lt3ring15g00d4ursoul";
const userId = "@sir_arthur_david:humming.tiger";
+35 -20
View File
@@ -15,25 +15,37 @@ limitations under the License.
*/
"use strict";
const q = require("q");
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const utils = require("../test-utils");
const InteractiveAuth = sdk.InteractiveAuth;
const MatrixError = sdk.MatrixError;
import expect from 'expect';
// Trivial client object to test interactive auth
// (we do not need TestClient here)
class FakeClient {
generateClientSecret() {
return "testcl1Ent5EcreT";
}
}
describe("InteractiveAuth", function() {
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
});
it("should start an auth stage and complete it", function(done) {
const doRequest = jasmine.createSpy('doRequest');
const startAuthStage = jasmine.createSpy('startAuthStage');
const doRequest = expect.createSpy();
const stateUpdated = expect.createSpy();
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
doRequest: doRequest,
startAuthStage: startAuthStage,
stateUpdated: stateUpdated,
authData: {
session: "sessionId",
flows: [
@@ -51,7 +63,8 @@ describe("InteractiveAuth", function() {
});
// first we expect a call here
startAuthStage.andCallFake(function(stage) {
stateUpdated.andCall(function(stage) {
console.log('aaaa');
expect(stage).toEqual("logintype");
ia.submitAuthDict({
type: "logintype",
@@ -61,38 +74,40 @@ describe("InteractiveAuth", function() {
// .. which should trigger a call here
const requestRes = {"a": "b"};
doRequest.andCallFake(function(authData) {
doRequest.andCall(function(authData) {
console.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
foo: "bar",
});
return q(requestRes);
return Promise.resolve(requestRes);
});
ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest.calls.length).toEqual(1);
expect(startAuthStage.calls.length).toEqual(1);
}).catch(utils.failTest).done(done);
expect(stateUpdated.calls.length).toEqual(1);
}).nodeify(done);
});
it("should make a request if no authdata is provided", function(done) {
const doRequest = jasmine.createSpy('doRequest');
const startAuthStage = jasmine.createSpy('startAuthStage');
const doRequest = expect.createSpy();
const stateUpdated = expect.createSpy();
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
stateUpdated: stateUpdated,
doRequest: doRequest,
startAuthStage: startAuthStage,
});
expect(ia.getSessionId()).toBe(undefined);
expect(ia.getStageParams("logintype")).toBe(undefined);
// first we expect a call to doRequest
doRequest.andCallFake(function(authData) {
doRequest.andCall(function(authData) {
console.log("request1", authData);
expect(authData).toBe(null);
expect(authData).toEqual({});
const err = new MatrixError({
session: "sessionId",
flows: [
@@ -106,9 +121,9 @@ describe("InteractiveAuth", function() {
throw err;
});
// .. which should be followed by a call to startAuthStage
// .. which should be followed by a call to stateUpdated
const requestRes = {"a": "b"};
startAuthStage.andCallFake(function(stage) {
stateUpdated.andCall(function(stage) {
expect(stage).toEqual("logintype");
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams("logintype")).toEqual({
@@ -116,14 +131,14 @@ describe("InteractiveAuth", function() {
});
// submitAuthDict should trigger another call to doRequest
doRequest.andCallFake(function(authData) {
doRequest.andCall(function(authData) {
console.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
foo: "bar",
});
return q(requestRes);
return Promise.resolve(requestRes);
});
ia.submitAuthDict({
@@ -135,7 +150,7 @@ describe("InteractiveAuth", function() {
ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest.calls.length).toEqual(2);
expect(startAuthStage.calls.length).toEqual(1);
}).catch(utils.failTest).done(done);
expect(stateUpdated.calls.length).toEqual(1);
}).nodeify(done);
});
});
+69 -49
View File
@@ -1,9 +1,13 @@
"use strict";
const q = require("q");
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
const utils = require("../test-utils");
import expect from 'expect';
import lolex from 'lolex';
describe("MatrixClient", function() {
const userId = "@alice:bar";
const identityServerUrl = "https://identity.server";
@@ -11,6 +15,7 @@ describe("MatrixClient", function() {
let client;
let store;
let scheduler;
let clock;
const KEEP_ALIVE_PATH = "/_matrix/client/versions";
@@ -57,7 +62,7 @@ describe("MatrixClient", function() {
let pendingLookup = null;
function httpReq(cb, method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
return q();
return Promise.resolve();
}
const next = httpLookups.shift();
const logLine = (
@@ -79,7 +84,7 @@ describe("MatrixClient", function() {
);
}
pendingLookup = {
promise: q.defer().promise,
promise: Promise.defer().promise,
method: method,
path: path,
};
@@ -104,7 +109,7 @@ describe("MatrixClient", function() {
}
if (next.error) {
return q.reject({
return Promise.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
name: next.error.errcode,
@@ -112,24 +117,31 @@ describe("MatrixClient", function() {
data: next.error,
});
}
return q(next.data);
return Promise.resolve(next.data);
}
expect(true).toBe(false, "Expected different request. " + logLine);
return q.defer().promise;
return Promise.defer().promise;
}
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
jasmine.Clock.useMock();
scheduler = jasmine.createSpyObj("scheduler", [
clock = lolex.install();
scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
]);
store = jasmine.createSpyObj("store", [
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
store = [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"setSyncToken", "storeEvents", "storeRoom", "storeUser",
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser",
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
]);
"getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
store.getClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
store.storeClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
store.isNewlyCreated = expect.createSpy().andReturn(Promise.resolve(true));
client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
@@ -140,14 +152,14 @@ describe("MatrixClient", function() {
userId: userId,
});
// FIXME: We shouldn't be yanking _http like this.
client._http = jasmine.createSpyObj("httpApi", [
client._http = [
"authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent",
]);
client._http.authedRequest.andCallFake(httpReq);
client._http.authedRequestWithPrefix.andCallFake(httpReq);
client._http.requestWithPrefix.andCallFake(httpReq);
client._http.request.andCallFake(httpReq);
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
client._http.authedRequest.andCall(httpReq);
client._http.authedRequestWithPrefix.andCall(httpReq);
client._http.requestWithPrefix.andCall(httpReq);
client._http.request.andCall(httpReq);
// set reasonable working defaults
acceptKeepalives = true;
@@ -159,20 +171,21 @@ describe("MatrixClient", function() {
});
afterEach(function() {
clock.uninstall();
// need to re-stub the requests with NOPs because there are no guarantees
// clients from previous tests will be GC'd before the next test. This
// 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.andCallFake(function() {
return q.defer().promise;
client._http.authedRequest.andCall(function() {
return Promise.defer().promise;
});
client._http.authedRequestWithPrefix.andCallFake(function() {
return q.defer().promise;
client._http.authedRequestWithPrefix.andCall(function() {
return Promise.defer().promise;
});
});
it("should not POST /filter if a matching filter already exists", function(done) {
it("should not POST /filter if a matching filter already exists", async function() {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
@@ -181,31 +194,38 @@ describe("MatrixClient", function() {
const filter = new sdk.Filter(0, filterId);
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
store.getFilter.andReturn(filter);
client.startClient();
client.on("sync", function syncListener(state) {
if (state === "SYNCING") {
expect(httpLookups.length).toEqual(0);
client.removeListener("sync", syncListener);
done();
}
const syncPromise = new Promise((resolve, reject) => {
client.on("sync", function syncListener(state) {
if (state === "SYNCING") {
expect(httpLookups.length).toEqual(0);
client.removeListener("sync", syncListener);
resolve();
} else if (state === "ERROR") {
reject(new Error("sync error"));
}
});
});
await client.startClient();
await syncPromise;
});
describe("getSyncState", function() {
it("should return null if the client isn't started", function() {
expect(client.getSyncState()).toBeNull();
expect(client.getSyncState()).toBe(null);
});
it("should return the same sync state as emitted sync events", function(done) {
client.on("sync", function syncListener(state) {
expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") {
client.removeListener("sync", syncListener);
done();
}
it("should return the same sync state as emitted sync events", async function() {
const syncingPromise = new Promise((resolve) => {
client.on("sync", function syncListener(state) {
expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") {
client.removeListener("sync", syncListener);
resolve();
}
});
});
client.startClient();
await client.startClient();
await syncingPromise;
});
});
@@ -248,8 +268,8 @@ describe("MatrixClient", function() {
});
describe("retryImmediately", function() {
it("should return false if there is no request waiting", function() {
client.startClient();
it("should return false if there is no request waiting", async function() {
await client.startClient();
expect(client.retryImmediately()).toBe(false);
});
@@ -266,7 +286,7 @@ describe("MatrixClient", function() {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(2);
expect(client.retryImmediately()).toBe(true);
jasmine.Clock.tick(1);
clock.tick(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
@@ -292,9 +312,9 @@ describe("MatrixClient", function() {
expect(client.retryImmediately()).toBe(
true, "retryImmediately returned false",
);
jasmine.Clock.tick(1);
clock.tick(1);
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
jasmine.Clock.tick(10000);
clock.tick(10000);
} else if (state === "SYNCING" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
@@ -316,7 +336,7 @@ describe("MatrixClient", function() {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(3);
expect(client.retryImmediately()).toBe(true);
jasmine.Clock.tick(1);
clock.tick(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
@@ -347,7 +367,7 @@ describe("MatrixClient", function() {
done();
}
// standard retry time is 5 to 10 seconds
jasmine.Clock.tick(10000);
clock.tick(10000);
};
}
@@ -370,7 +390,7 @@ describe("MatrixClient", function() {
client.startClient();
});
it("should transition ERROR -> PREPARED after /sync if prev failed",
it("should transition ERROR -> CATCHUP after /sync if prev failed",
function(done) {
const expectedStates = [];
acceptKeepalives = false;
@@ -393,7 +413,7 @@ describe("MatrixClient", function() {
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["PREPARED", "ERROR"]);
expectedStates.push(["CATCHUP", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
+6
View File
@@ -1,7 +1,10 @@
"use strict";
import 'source-map-support/register';
const PushProcessor = require("../../lib/pushprocessor");
const utils = require("../test-utils");
import expect from 'expect';
describe('NotificationService', function() {
const testUserId = "@ali:matrix.org";
const testDisplayName = "Alice M";
@@ -21,6 +24,9 @@ describe('NotificationService', function() {
name: testDisplayName,
};
},
getJoinedMemberCount: function() {
return 0;
},
members: {},
},
};
+57 -53
View File
@@ -1,74 +1,75 @@
"use strict";
import 'source-map-support/register';
const callbacks = require("../../lib/realtime-callbacks");
const testUtils = require("../test-utils.js");
import expect from 'expect';
import lolex from 'lolex';
describe("realtime-callbacks", function() {
const clock = jasmine.Clock;
let fakeDate;
let clock;
function tick(millis) {
// make sure we tick the fakedate first, otherwise nothing will happen!
fakeDate += millis;
clock.tick(millis);
}
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
clock.useMock();
fakeDate = Date.now();
callbacks.setNow(function() {
return fakeDate;
});
clock = lolex.install();
const fakeDate = clock.Date;
callbacks.setNow(fakeDate.now.bind(fakeDate));
});
afterEach(function() {
callbacks.setNow();
clock.uninstall();
});
describe("setTimeout", function() {
it("should call the callback after the timeout", function() {
const callback = jasmine.createSpy();
const callback = expect.createSpy();
callbacks.setTimeout(callback, 100);
expect(callback).not.toHaveBeenCalled();
expect(callback).toNotHaveBeenCalled();
tick(100);
expect(callback).toHaveBeenCalled();
});
it("should default to a zero timeout", function() {
const callback = jasmine.createSpy();
const callback = expect.createSpy();
callbacks.setTimeout(callback);
expect(callback).not.toHaveBeenCalled();
expect(callback).toNotHaveBeenCalled();
tick(0);
expect(callback).toHaveBeenCalled();
});
it("should pass any parameters to the callback", function() {
const callback = jasmine.createSpy();
const callback = expect.createSpy();
callbacks.setTimeout(callback, 0, "a", "b", "c");
tick(0);
expect(callback).toHaveBeenCalledWith("a", "b", "c");
});
it("should set 'this' to the global object", function() {
const callback = jasmine.createSpy();
callback.andCallFake(function() {
let passed = false;
const callback = function() {
expect(this).toBe(global); // eslint-disable-line no-invalid-this
expect(this.console).toBeDefined(); // eslint-disable-line no-invalid-this
});
expect(this.console).toBeTruthy(); // eslint-disable-line no-invalid-this
passed = true;
};
callbacks.setTimeout(callback);
tick(0);
expect(callback).toHaveBeenCalled();
expect(passed).toBe(true);
});
it("should handle timeouts of several seconds", function() {
const callback = jasmine.createSpy();
const callback = expect.createSpy();
callbacks.setTimeout(callback, 2000);
expect(callback).not.toHaveBeenCalled();
expect(callback).toNotHaveBeenCalled();
for (let i = 0; i < 4; i++) {
tick(500);
}
@@ -76,24 +77,24 @@ describe("realtime-callbacks", function() {
});
it("should call multiple callbacks in the right order", function() {
const callback1 = jasmine.createSpy("callback1");
const callback2 = jasmine.createSpy("callback2");
const callback3 = jasmine.createSpy("callback3");
const callback1 = expect.createSpy();
const callback2 = expect.createSpy();
const callback3 = expect.createSpy();
callbacks.setTimeout(callback2, 200);
callbacks.setTimeout(callback1, 100);
callbacks.setTimeout(callback3, 300);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
expect(callback1).toNotHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
expect(callback3).toNotHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
expect(callback3).toNotHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
expect(callback3).toNotHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
@@ -101,51 +102,54 @@ describe("realtime-callbacks", function() {
});
it("should treat -ve timeouts the same as a zero timeout", function() {
const callback1 = jasmine.createSpy("callback1");
const callback2 = jasmine.createSpy("callback2");
const callback1 = expect.createSpy();
const callback2 = expect.createSpy();
// check that cb1 is called before cb2
callback1.andCallFake(function() {
expect(callback2).not.toHaveBeenCalled();
callback1.andCall(function() {
expect(callback2).toNotHaveBeenCalled();
});
callbacks.setTimeout(callback1);
callbacks.setTimeout(callback2, -100);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback1).toNotHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
it("should not get confused by chained calls", function() {
const callback2 = jasmine.createSpy("callback2");
const callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
const callback2 = expect.createSpy();
const callback1 = expect.createSpy();
callback1.andCall(function() {
callbacks.setTimeout(callback2, 0);
expect(callback2).not.toHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
});
callbacks.setTimeout(callback1);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback1).toNotHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
// the fake timer won't actually run callbacks registered during
// one tick until the next tick.
tick(1);
expect(callback2).toHaveBeenCalled();
});
it("should be immune to exceptions", function() {
const callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
const callback1 = expect.createSpy();
callback1.andCall(function() {
throw new Error("prepare to die");
});
const callback2 = jasmine.createSpy("callback2");
const callback2 = expect.createSpy();
callbacks.setTimeout(callback1, 0);
callbacks.setTimeout(callback2, 0);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback1).toNotHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
@@ -154,16 +158,16 @@ describe("realtime-callbacks", function() {
describe("cancelTimeout", function() {
it("should cancel a pending timeout", function() {
const callback = jasmine.createSpy();
const callback = expect.createSpy();
const k = callbacks.setTimeout(callback);
callbacks.clearTimeout(k);
tick(0);
expect(callback).not.toHaveBeenCalled();
expect(callback).toNotHaveBeenCalled();
});
it("should not affect sooner timeouts", function() {
const callback1 = jasmine.createSpy("callback1");
const callback2 = jasmine.createSpy("callback2");
const callback1 = expect.createSpy();
const callback2 = expect.createSpy();
callbacks.setTimeout(callback1, 100);
const k = callbacks.setTimeout(callback2, 200);
@@ -171,10 +175,10 @@ describe("realtime-callbacks", function() {
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
tick(150);
expect(callback2).not.toHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
});
});
});
+15 -3
View File
@@ -1,8 +1,11 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const RoomMember = sdk.RoomMember;
const utils = require("../test-utils");
import expect from 'expect';
describe("RoomMember", function() {
const roomId = "!foo:bar";
const userA = "@alice:bar";
@@ -33,7 +36,7 @@ describe("RoomMember", function() {
const url = member.getAvatarUrl(hsUrl);
// we don't care about how the mxc->http conversion is done, other
// than it contains the mxc body.
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
expect(url.indexOf("flibble/wibble")).toNotEqual(-1);
});
it("should return an identicon HTTP URL if allowDefault was set and there " +
@@ -189,6 +192,15 @@ describe("RoomMember", function() {
});
});
describe("isOutOfBand", function() {
it("should be set by markOutOfBand", function() {
const member = new RoomMember();
expect(member.isOutOfBand()).toEqual(false);
member.markOutOfBand();
expect(member.isOutOfBand()).toEqual(true);
});
});
describe("setMembershipEvent", function() {
const joinEvent = utils.mkMembership({
event: true,
@@ -243,9 +255,9 @@ describe("RoomMember", function() {
member.setMembershipEvent(joinEvent);
expect(member.name).toEqual("Alice"); // prefer displayname
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alice"); // it should disambig.
expect(member.name).toNotEqual("Alice"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).not.toEqual(-1);
expect(member.name.indexOf(userA)).toNotEqual(-1);
});
it("should emit 'RoomMember.membership' if the membership changes", function() {
+233 -24
View File
@@ -1,13 +1,19 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const RoomState = sdk.RoomState;
const RoomMember = sdk.RoomMember;
const utils = require("../test-utils");
import expect from 'expect';
describe("RoomState", function() {
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bob:bar";
const userC = "@cleo:bar";
const userLazy = "@lazy:bar";
let state;
beforeEach(function() {
@@ -43,8 +49,8 @@ describe("RoomState", function() {
const members = state.getMembers();
expect(members.length).toEqual(2);
// ordering unimportant
expect([userA, userB].indexOf(members[0].userId)).not.toEqual(-1);
expect([userA, userB].indexOf(members[1].userId)).not.toEqual(-1);
expect([userA, userB].indexOf(members[0].userId)).toNotEqual(-1);
expect([userA, userB].indexOf(members[1].userId)).toNotEqual(-1);
});
});
@@ -54,7 +60,7 @@ describe("RoomState", function() {
});
it("should return a member if they exist", function() {
expect(state.getMember(userB)).toBeDefined();
expect(state.getMember(userB)).toBeTruthy();
});
it("should return a member which changes as state changes", function() {
@@ -75,8 +81,8 @@ describe("RoomState", function() {
});
describe("getSentinelMember", function() {
it("should return null if there is no member", function() {
expect(state.getSentinelMember("@no-one:here")).toEqual(null);
it("should return a member with the user id as name", function() {
expect(state.getSentinelMember("@no-one:here").name).toEqual("@no-one:here");
});
it("should return a member which doesn't change when the state is updated",
@@ -114,8 +120,8 @@ describe("RoomState", function() {
const events = state.getStateEvents("m.room.member");
expect(events.length).toEqual(2);
// ordering unimportant
expect([userA, userB].indexOf(events[0].getStateKey())).not.toEqual(-1);
expect([userA, userB].indexOf(events[1].getStateKey())).not.toEqual(-1);
expect([userA, userB].indexOf(events[0].getStateKey())).toNotEqual(-1);
expect([userA, userB].indexOf(events[1].getStateKey())).toNotEqual(-1);
});
it("should return a single MatrixEvent if a state_key was specified",
@@ -159,6 +165,7 @@ describe("RoomState", function() {
];
let emitCount = 0;
state.on("RoomState.newMember", function(ev, st, mem) {
expect(state.getMember(mem.userId)).toEqual(mem);
expect(mem.userId).toEqual(memberEvents[emitCount].getSender());
expect(mem.membership).toBeFalsy(); // not defined yet
emitCount += 1;
@@ -219,7 +226,6 @@ describe("RoomState", function() {
it("should call setPowerLevelEvent on a new RoomMember if power levels exist",
function() {
const userC = "@cleo:bar";
const memberEvent = utils.mkMembership({
mship: "join", user: userC, room: roomId, event: true,
});
@@ -238,7 +244,7 @@ describe("RoomState", function() {
// TODO: We do this because we don't DI the RoomMember constructor
// so we can't inject a mock :/ so we have to infer.
expect(state.members[userC]).toBeDefined();
expect(state.members[userC]).toBeTruthy();
expect(state.members[userC].powerLevel).toEqual(10);
});
@@ -252,13 +258,121 @@ describe("RoomState", function() {
});
state.setStateEvents([memberEvent]);
expect(state.members[userA].setMembershipEvent).not.toHaveBeenCalled();
expect(state.members[userA].setMembershipEvent).toNotHaveBeenCalled();
expect(state.members[userB].setMembershipEvent).toHaveBeenCalledWith(
memberEvent, state,
);
});
});
describe("setOutOfBandMembers", function() {
it("should add a new member", function() {
const oobMemberEvent = utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
});
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
const member = state.getMember(userLazy);
expect(member.userId).toEqual(userLazy);
expect(member.isOutOfBand()).toEqual(true);
});
it("should have no effect when not in correct status", function() {
state.setOutOfBandMembers([utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
})]);
expect(state.getMember(userLazy)).toBeFalsy();
});
it("should emit newMember when adding a member", function() {
const userLazy = "@oob:hs";
const oobMemberEvent = utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
});
let eventReceived = false;
state.once('RoomState.newMember', (_, __, member) => {
expect(member.userId).toEqual(userLazy);
eventReceived = true;
});
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
expect(eventReceived).toEqual(true);
});
it("should never overwrite existing members", function() {
const oobMemberEvent = utils.mkMembership({
user: userA, mship: "join", room: roomId, event: true,
});
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
const memberA = state.getMember(userA);
expect(memberA.events.member.getId()).toNotEqual(oobMemberEvent.getId());
expect(memberA.isOutOfBand()).toEqual(false);
});
it("should emit members when updating a member", function() {
const doesntExistYetUserId = "@doesntexistyet:hs";
const oobMemberEvent = utils.mkMembership({
user: doesntExistYetUserId, mship: "join", room: roomId, event: true,
});
let eventReceived = false;
state.once('RoomState.members', (_, __, member) => {
expect(member.userId).toEqual(doesntExistYetUserId);
eventReceived = true;
});
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
expect(eventReceived).toEqual(true);
});
});
describe("clone", function() {
it("should contain same information as original", function() {
// include OOB members in copy
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
})]);
const copy = state.clone();
// check individual members
[userA, userB, userLazy].forEach((userId) => {
const member = state.getMember(userId);
const memberCopy = copy.getMember(userId);
expect(member.name).toEqual(memberCopy.name);
expect(member.isOutOfBand()).toEqual(memberCopy.isOutOfBand());
});
// check member keys
expect(Object.keys(state.members)).toEqual(Object.keys(copy.members));
// check join count
expect(state.getJoinedMemberCount()).toEqual(copy.getJoinedMemberCount());
});
it("should mark old copy as not waiting for out of band anymore", function() {
state.markOutOfBandMembersStarted();
const copy = state.clone();
copy.setOutOfBandMembers([utils.mkMembership({
user: userA, mship: "join", room: roomId, event: true,
})]);
// should have no effect as it should be marked in status finished just like copy
state.setOutOfBandMembers([utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
})]);
expect(state.getMember(userLazy)).toBeFalsy();
});
it("should return copy independent of original", function() {
const copy = state.clone();
copy.setStateEvents([utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
})]);
expect(state.getMember(userLazy)).toBeFalsy();
expect(state.getJoinedMemberCount()).toEqual(2);
expect(copy.getJoinedMemberCount()).toEqual(3);
});
});
describe("setTypingEvent", function() {
it("should call setTypingEvent on each RoomMember", function() {
const typingEvent = utils.mkEvent({
@@ -281,13 +395,6 @@ describe("RoomState", function() {
});
describe("maySendStateEvent", function() {
it("should say non-joined members may not send state",
function() {
expect(state.maySendStateEvent(
'm.room.name', "@nobody:nowhere",
)).toEqual(false);
});
it("should say any member may send state with no power level event",
function() {
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
@@ -363,15 +470,117 @@ describe("RoomState", function() {
});
});
describe("maySendEvent", function() {
it("should say non-joined members may not send events",
function() {
expect(state.maySendEvent(
'm.room.message', "@nobody:nowhere",
)).toEqual(false);
expect(state.maySendMessage("@nobody:nowhere")).toEqual(false);
describe("getJoinedMemberCount", function() {
beforeEach(() => {
state = new RoomState(roomId);
});
it("should update after adding joined member", function() {
state.setStateEvents([
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}),
]);
expect(state.getJoinedMemberCount()).toEqual(2);
});
});
describe("getInvitedMemberCount", function() {
beforeEach(() => {
state = new RoomState(roomId);
});
it("should update after adding invited member", function() {
state.setStateEvents([
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}),
]);
expect(state.getInvitedMemberCount()).toEqual(2);
});
});
describe("setJoinedMemberCount", function() {
beforeEach(() => {
state = new RoomState(roomId);
});
it("should, once used, override counting members from state", function() {
state.setStateEvents([
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}),
]);
expect(state.getJoinedMemberCount()).toEqual(100);
});
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}),
]);
state.setJoinedMemberCount(100);
const copy = state.clone();
copy.setStateEvents([
utils.mkMembership({event: true, mship: "join",
user: userC, room: roomId}),
]);
expect(state.getJoinedMemberCount()).toEqual(100);
});
});
describe("setInvitedMemberCount", function() {
beforeEach(() => {
state = new RoomState(roomId);
});
it("should, once used, override counting members from state", function() {
state.setStateEvents([
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}),
]);
expect(state.getInvitedMemberCount()).toEqual(100);
});
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}),
]);
state.setInvitedMemberCount(100);
const copy = state.clone();
copy.setStateEvents([
utils.mkMembership({event: true, mship: "invite",
user: userC, room: roomId}),
]);
expect(state.getInvitedMemberCount()).toEqual(100);
});
});
describe("maySendEvent", function() {
it("should say any member may send events with no power level event",
function() {
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
+355 -161
View File
@@ -1,4 +1,5 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const Room = sdk.Room;
const RoomState = sdk.RoomState;
@@ -7,6 +8,8 @@ const EventStatus = sdk.EventStatus;
const EventTimeline = sdk.EventTimeline;
const utils = require("../test-utils");
import expect from 'expect';
describe("Room", function() {
const roomId = "!foo:bar";
const userA = "@alice:bar";
@@ -29,7 +32,7 @@ describe("Room", function() {
const hsUrl = "https://my.home.server";
it("should return the URL from m.room.avatar preferentially", function() {
room.currentState.getStateEvents.andCallFake(function(type, key) {
room.currentState.getStateEvents.andCall(function(type, key) {
if (type === "m.room.avatar" && key === "") {
return utils.mkEvent({
event: true,
@@ -46,7 +49,7 @@ describe("Room", function() {
const url = room.getAvatarUrl(hsUrl);
// we don't care about how the mxc->http conversion is done, other
// than it contains the mxc body.
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
expect(url.indexOf("flibble/wibble")).toNotEqual(-1);
});
it("should return an identicon HTTP URL if allowDefault was set and there " +
@@ -64,13 +67,14 @@ describe("Room", function() {
describe("getMember", function() {
beforeEach(function() {
// clobber members property with test data
room.currentState.members = {
"@alice:bar": {
userId: userA,
roomId: roomId,
},
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": {
userId: userA,
roomId: roomId,
},
}[userId];
});
});
it("should return null if the member isn't in current state", function() {
@@ -78,7 +82,7 @@ describe("Room", function() {
});
it("should return the member from current state", function() {
expect(room.getMember(userA)).not.toEqual(null);
expect(room.getMember(userA)).toNotEqual(null);
});
});
@@ -170,7 +174,7 @@ describe("Room", function() {
);
expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(true);
expect(room.oldState.setStateEvents).not.toHaveBeenCalled();
expect(room.oldState.setStateEvents).toNotHaveBeenCalled();
});
it("should synthesize read receipts for the senders of events", function() {
@@ -179,7 +183,7 @@ describe("Room", function() {
membership: "join",
name: "Alice",
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
room.currentState.getSentinelMember.andCall(function(uid) {
if (uid === userA) {
return sentinel;
}
@@ -288,13 +292,13 @@ describe("Room", function() {
membership: "join",
name: "Old Alice",
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
room.currentState.getSentinelMember.andCall(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.andCallFake(function(uid) {
room.oldState.getSentinelMember.andCall(function(uid) {
if (uid === userA) {
return oldSentinel;
}
@@ -327,13 +331,13 @@ describe("Room", function() {
membership: "join",
name: "Old Alice",
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
room.currentState.getSentinelMember.andCall(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.andCallFake(function(uid) {
room.oldState.getSentinelMember.andCall(function(uid) {
if (uid === userA) {
return oldSentinel;
}
@@ -375,33 +379,36 @@ describe("Room", function() {
);
expect(events[0].forwardLooking).toBe(false);
expect(events[1].forwardLooking).toBe(false);
expect(room.currentState.setStateEvents).not.toHaveBeenCalled();
expect(room.currentState.setStateEvents).toNotHaveBeenCalled();
});
});
const resetTimelineTests = function(timelineSupport) {
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "A message", event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" },
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Another New Name" },
}),
];
let events = null;
beforeEach(function() {
room = new Room(roomId, {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 = [
utils.mkMessage({
room: roomId, user: userA, msg: "A message", event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" },
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Another New Name" },
}),
];
});
it("should copy state from previous timeline", function() {
room.addLiveEvents([events[0], events[1]]);
expect(room.getLiveTimeline().getEvents().length).toEqual(2);
room.resetLiveTimeline();
room.resetLiveTimeline('sometoken', 'someothertoken');
room.addLiveEvents([events[2]]);
const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS);
@@ -414,7 +421,7 @@ describe("Room", function() {
it("should reset the legacy timeline fields", function() {
room.addLiveEvents([events[0], events[1]]);
expect(room.timeline.length).toEqual(2);
room.resetLiveTimeline();
room.resetLiveTimeline('sometoken', 'someothertoken');
room.addLiveEvents([events[2]]);
const newLiveTimeline = room.getLiveTimeline();
@@ -448,7 +455,7 @@ describe("Room", function() {
room.addLiveEvents([events[0]]);
expect(room.timeline.length).toEqual(1);
const firstLiveTimeline = room.getLiveTimeline();
room.resetLiveTimeline();
room.resetLiveTimeline('sometoken', 'someothertoken');
const tl = room.getTimelineForEvent(events[0].getId());
expect(tl).toBe(timelineSupport ? firstLiveTimeline : null);
@@ -462,7 +469,7 @@ describe("Room", function() {
describe("compareEventOrdering", function() {
beforeEach(function() {
room = new Room(roomId, {timelineSupport: true});
room = new Room(roomId, null, null, {timelineSupport: true});
});
const events = [
@@ -538,7 +545,7 @@ describe("Room", function() {
describe("getJoinedMembers", function() {
it("should return members whose membership is 'join'", function() {
room.currentState.getMembers.andCallFake(function() {
room.currentState.getMembers.andCall(function() {
return [
{ userId: "@alice:bar", membership: "join" },
{ userId: "@bob:bar", membership: "invite" },
@@ -551,7 +558,7 @@ describe("Room", function() {
});
it("should return an empty list if no membership is 'join'", function() {
room.currentState.getMembers.andCallFake(function() {
room.currentState.getMembers.andCall(function() {
return [
{ userId: "@bob:bar", membership: "invite" },
];
@@ -564,72 +571,75 @@ describe("Room", function() {
describe("hasMembershipState", function() {
it("should return true for a matching userId and membership",
function() {
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
"@bob:bar": { userId: "@bob:bar", membership: "invite" },
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
"@bob:bar": { userId: "@bob:bar", membership: "invite" },
}[userId];
});
expect(room.hasMembershipState("@bob:bar", "invite")).toBe(true);
});
it("should return false if match membership but no match userId",
function() {
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId];
});
expect(room.hasMembershipState("@bob:bar", "join")).toBe(false);
});
it("should return false if match userId but no match membership",
function() {
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId];
});
expect(room.hasMembershipState("@alice:bar", "ban")).toBe(false);
});
it("should return false if no match membership or userId",
function() {
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId];
});
expect(room.hasMembershipState("@bob:bar", "invite")).toBe(false);
});
it("should return false if no members exist",
function() {
room.currentState.members = {};
expect(room.hasMembershipState("@foo:bar", "join")).toBe(false);
});
});
describe("recalculate", function() {
let stateLookup = {
// event.type + "$" event.state_key : MatrixEvent
};
const setJoinRule = function(rule) {
stateLookup["m.room.join_rules$"] = utils.mkEvent({
room.addLiveEvents([utils.mkEvent({
type: "m.room.join_rules", room: roomId, user: userA, content: {
join_rule: rule,
}, event: true,
});
})]);
};
const setAliases = function(aliases, stateKey) {
if (!stateKey) {
stateKey = "flibble";
}
stateLookup["m.room.aliases$" + stateKey] = utils.mkEvent({
room.addLiveEvents([utils.mkEvent({
type: "m.room.aliases", room: roomId, skey: stateKey, content: {
aliases: aliases,
}, event: true,
});
})]);
};
const setRoomName = function(name) {
stateLookup["m.room.name$"] = utils.mkEvent({
room.addLiveEvents([utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, content: {
name: name,
}, event: true,
});
})]);
};
const addMember = function(userId, state, opts) {
if (!state) {
@@ -641,56 +651,14 @@ describe("Room", function() {
opts.user = opts.user || userId;
opts.skey = userId;
opts.event = true;
stateLookup["m.room.member$" + userId] = utils.mkMembership(opts);
const event = utils.mkMembership(opts);
room.addLiveEvents([event]);
return event;
};
beforeEach(function() {
stateLookup = {};
room.currentState.getStateEvents.andCallFake(function(type, key) {
if (key === undefined) {
const prefix = type + "$";
const list = [];
for (const stateBlob in stateLookup) {
if (!stateLookup.hasOwnProperty(stateBlob)) {
continue;
}
if (stateBlob.indexOf(prefix) === 0) {
list.push(stateLookup[stateBlob]);
}
}
return list;
} else {
return stateLookup[type + "$" + key];
}
});
room.currentState.getMembers.andCallFake(function() {
const memberEvents = room.currentState.getStateEvents("m.room.member");
const members = [];
for (let i = 0; i < memberEvents.length; i++) {
members.push({
name: memberEvents[i].event.content &&
memberEvents[i].event.content.displayname ?
memberEvents[i].event.content.displayname :
memberEvents[i].getStateKey(),
userId: memberEvents[i].getStateKey(),
events: { member: memberEvents[i] },
});
}
return members;
});
room.currentState.getMember.andCallFake(function(userId) {
const memberEvent = room.currentState.getStateEvents(
"m.room.member", userId,
);
return {
name: memberEvent.event.content &&
memberEvent.event.content.displayname ?
memberEvent.event.content.displayname :
memberEvent.getStateKey(),
userId: memberEvent.getStateKey(),
events: { member: memberEvent },
};
});
// no mocking
room = new Room(roomId, null, userA);
});
describe("Room.recalculate => Stripped State Events", function() {
@@ -698,8 +666,8 @@ describe("Room", function() {
"room is an invite room", function() {
const roomName = "flibble";
addMember(userA, "invite");
stateLookup["m.room.member$" + userA].event.invite_room_state = [
const event = addMember(userA, "invite");
event.event.invite_room_state = [
{
type: "m.room.name",
state_key: "",
@@ -709,29 +677,108 @@ describe("Room", function() {
},
];
room.recalculate(userA);
expect(room.currentState.setStateEvents).toHaveBeenCalled();
// first call, first arg (which is an array), first element in array
const fakeEvent = room.currentState.setStateEvents.calls[0].args[0][0];
expect(fakeEvent.getContent()).toEqual({
name: roomName,
});
room.recalculate();
expect(room.name).toEqual(roomName);
});
it("should not clobber state events if it isn't an invite room", function() {
addMember(userA, "join");
stateLookup["m.room.member$" + userA].event.invite_room_state = [
const event = addMember(userA, "join");
const roomName = "flibble";
setRoomName(roomName);
const roomNameToIgnore = "ignoreme";
event.event.invite_room_state = [
{
type: "m.room.name",
state_key: "",
content: {
name: "flibble",
name: roomNameToIgnore,
},
},
];
room.recalculate(userA);
expect(room.currentState.setStateEvents).not.toHaveBeenCalled();
room.recalculate();
expect(room.name).toEqual(roomName);
});
});
describe("Room.recalculate => Room Name using room summary", function() {
it("should use room heroes if available", function() {
addMember(userA, "invite");
addMember(userB);
addMember(userC);
addMember(userD);
room.setSummary({
"m.heroes": [userB, userC, userD],
});
room.recalculate();
expect(room.name).toEqual(`${userB} and 2 others`);
});
it("missing hero member state reverts to mxid", function() {
room.setSummary({
"m.heroes": [userB],
"m.joined_member_count": 2,
});
room.recalculate();
expect(room.name).toEqual(userB);
});
it("uses hero name from state", function() {
const name = "Mr B";
addMember(userA, "invite");
addMember(userB, "join", {name});
room.setSummary({
"m.heroes": [userB],
});
room.recalculate();
expect(room.name).toEqual(name);
});
it("uses counts from summary", function() {
const name = "Mr B";
addMember(userB, "join", {name});
room.setSummary({
"m.heroes": [userB],
"m.joined_member_count": 50,
"m.invited_member_count": 50,
});
room.recalculate();
expect(room.name).toEqual(`${name} and 98 others`);
});
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});
room.setSummary({
"m.heroes": [userB, userC],
});
room.recalculate();
expect(room.name).toEqual(`${nameB} and ${nameC}`);
});
it("uses only heroes", function() {
const nameB = "Mr Bean";
addMember(userB, "join", {name: nameB});
addMember(userC, "join");
room.setSummary({
"m.heroes": [userB],
});
room.recalculate();
expect(room.name).toEqual(nameB);
});
it("reverts to empty room in case of self chat", function() {
room.setSummary({
"m.heroes": [],
"m.invited_member_count": 1,
});
room.recalculate();
expect(room.name).toEqual("Empty room");
});
});
@@ -744,7 +791,7 @@ describe("Room", function() {
addMember(userB);
addMember(userC);
addMember(userD);
room.recalculate(userA);
room.recalculate();
const name = room.name;
// we expect at least 1 member to be mentioned
const others = [userB, userC, userD];
@@ -765,10 +812,10 @@ describe("Room", function() {
addMember(userA);
addMember(userB);
addMember(userC);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).not.toEqual(-1, name);
expect(name.indexOf(userC)).not.toEqual(-1, name);
expect(name.indexOf(userB)).toNotEqual(-1, name);
expect(name.indexOf(userC)).toNotEqual(-1, name);
});
it("should return the names of members in a public (public join_rules)" +
@@ -778,10 +825,10 @@ describe("Room", function() {
addMember(userA);
addMember(userB);
addMember(userC);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).not.toEqual(-1, name);
expect(name.indexOf(userC)).not.toEqual(-1, name);
expect(name.indexOf(userB)).toNotEqual(-1, name);
expect(name.indexOf(userC)).toNotEqual(-1, name);
});
it("should show the other user's name for public (public join_rules)" +
@@ -790,9 +837,9 @@ describe("Room", function() {
setJoinRule("public");
addMember(userA);
addMember(userB);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).not.toEqual(-1, name);
expect(name.indexOf(userB)).toNotEqual(-1, name);
});
it("should show the other user's name for private " +
@@ -801,9 +848,9 @@ describe("Room", function() {
setJoinRule("invite");
addMember(userA);
addMember(userB);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).not.toEqual(-1, name);
expect(name.indexOf(userB)).toNotEqual(-1, name);
});
it("should show the other user's name for private" +
@@ -811,9 +858,9 @@ describe("Room", function() {
setJoinRule("invite");
addMember(userA, "invite", {user: userB});
addMember(userB);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).not.toEqual(-1, name);
expect(name.indexOf(userB)).toNotEqual(-1, name);
});
it("should show the room alias if one exists for private " +
@@ -821,7 +868,7 @@ describe("Room", function() {
const alias = "#room_alias:here";
setJoinRule("invite");
setAliases([alias, "#another:one"]);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
});
@@ -831,7 +878,7 @@ describe("Room", function() {
const alias = "#room_alias:here";
setJoinRule("public");
setAliases([alias, "#another:one"]);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
});
@@ -841,7 +888,7 @@ describe("Room", function() {
const roomName = "A mighty name indeed";
setJoinRule("invite");
setRoomName(roomName);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual(roomName);
});
@@ -851,25 +898,23 @@ describe("Room", function() {
const roomName = "A mighty name indeed";
setJoinRule("public");
setRoomName(roomName);
room.recalculate(userA);
const name = room.name;
expect(name).toEqual(roomName);
room.recalculate();
expect(room.name).toEqual(roomName);
});
it("should return 'Empty room' for private (invite join_rules) rooms if" +
" a room name and alias don't exist and it is a self-chat.", function() {
setJoinRule("invite");
addMember(userA);
room.recalculate(userA);
const name = room.name;
expect(name).toEqual("Empty room");
room.recalculate();
expect(room.name).toEqual("Empty room");
});
it("should return 'Empty room' for public (public join_rules) rooms if a" +
" room name and alias don't exist and it is a self-chat.", function() {
setJoinRule("public");
addMember(userA);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual("Empty room");
});
@@ -877,30 +922,30 @@ describe("Room", function() {
it("should return 'Empty room' if there is no name, " +
"alias or members in the room.",
function() {
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual("Empty room");
});
it("should return 'Invite from [inviter display name] if state event " +
it("should return '[inviter display name] if state event " +
"available",
function() {
setJoinRule("invite");
addMember(userA, 'join', {name: "Alice"});
addMember(userB, "invite", {user: userA});
room.recalculate(userB);
addMember(userB, 'join', {name: "Alice"});
addMember(userA, "invite", {user: userA});
room.recalculate();
const name = room.name;
expect(name).toEqual("Invite from Alice");
expect(name).toEqual("Alice");
});
it("should return inviter mxid if display name not available",
function() {
setJoinRule("invite");
addMember(userA);
addMember(userB, "invite", {user: userA});
room.recalculate(userB);
addMember(userB);
addMember(userA, "invite", {user: userA});
room.recalculate();
const name = room.name;
expect(name).toEqual("Invite from " + userA);
expect(name).toEqual(userB);
});
});
});
@@ -959,7 +1004,7 @@ describe("Room", function() {
it("should emit an event when a receipt is added",
function() {
const listener = jasmine.createSpy('spy');
const listener = expect.createSpy();
room.on("Room.receipt", listener);
const ts = 13787898424;
@@ -1130,7 +1175,7 @@ describe("Room", function() {
it("should emit Room.tags event when new tags are " +
"received on the event stream",
function() {
const listener = jasmine.createSpy('spy');
const listener = expect.createSpy();
room.on("Room.tags", listener);
const tags = { "m.foo": { "order": 0.5 } };
@@ -1147,7 +1192,7 @@ describe("Room", function() {
describe("addPendingEvent", function() {
it("should add pending events to the pendingEventList if " +
"pendingEventOrdering == 'detached'", function() {
const room = new Room(roomId, {
const room = new Room(roomId, null, userA, {
pendingEventOrdering: "detached",
});
const eventA = utils.mkMessage({
@@ -1173,7 +1218,7 @@ describe("Room", function() {
it("should add pending events to the timeline if " +
"pendingEventOrdering == 'chronological'", function() {
room = new Room(roomId, {
room = new Room(roomId, null, userA, {
pendingEventOrdering: "chronological",
});
const eventA = utils.mkMessage({
@@ -1197,7 +1242,7 @@ describe("Room", function() {
describe("updatePendingEvent", function() {
it("should remove cancelled events from the pending list", function() {
const room = new Room(roomId, {
const room = new Room(roomId, null, userA, {
pendingEventOrdering: "detached",
});
const eventA = utils.mkMessage({
@@ -1233,7 +1278,7 @@ describe("Room", function() {
it("should remove cancelled events from the timeline", function() {
const room = new Room(roomId);
const room = new Room(roomId, null, userA);
const eventA = utils.mkMessage({
room: roomId, user: userA, event: true,
});
@@ -1265,4 +1310,153 @@ describe("Room", function() {
expect(callCount).toEqual(1);
});
});
describe("loadMembersIfNeeded", function() {
function createClientMock(serverResponse, storageResponse = null) {
return {
getEventMapper: function() {
// events should already be MatrixEvents
return function(event) {return event;};
},
isRoomEncrypted: function() {
return false;
},
_http: {
serverResponse,
authedRequest: function() {
if (this.serverResponse instanceof Error) {
return Promise.reject(this.serverResponse);
} else {
return Promise.resolve({chunk: this.serverResponse});
}
},
},
store: {
storageResponse,
storedMembers: null,
getOutOfBandMembers: function() {
if (this.storageResponse instanceof Error) {
return Promise.reject(this.storageResponse);
} else {
return Promise.resolve(this.storageResponse);
}
},
setOutOfBandMembers: function(roomId, memberEvents) {
this.storedMembers = memberEvents;
return Promise.resolve();
},
getSyncToken: () => "sync_token",
},
};
}
const memberEvent = utils.mkMembership({
user: "@user_a:bar", mship: "join",
room: roomId, event: true, name: "User A",
});
it("should load members from server on first call", async function() {
const client = createClientMock([memberEvent]);
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");
const storedMembers = client.store.storedMembers;
expect(storedMembers.length).toEqual(1);
expect(storedMembers[0].event_id).toEqual(memberEvent.getId());
});
it("should take members from storage if available", async function() {
const memberEvent2 = utils.mkMembership({
user: "@user_a:bar", mship: "join",
room: roomId, event: true, name: "Ms A",
});
const client = createClientMock([memberEvent2], [memberEvent]);
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");
});
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});
let hasThrown = false;
try {
await room.loadMembersIfNeeded();
} catch(err) {
hasThrown = true;
}
expect(hasThrown).toEqual(true);
client._http.serverResponse = [memberEvent];
await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar");
expect(memberA.name).toEqual("User A");
});
});
describe("getMyMembership", function() {
it("should return synced membership if membership isn't available yet",
function() {
const room = new Room(roomId, null, userA);
room.updateMyMembership("invite");
expect(room.getMyMembership()).toEqual("invite");
});
it("should emit a Room.myMembership event on a change",
function() {
const room = new Room(roomId, null, userA);
const events = [];
room.on("Room.myMembership", (_room, membership, oldMembership) => {
events.push({membership, oldMembership});
});
room.updateMyMembership("invite");
expect(room.getMyMembership()).toEqual("invite");
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"});
});
});
describe("guessDMUserId", function() {
it("should return first hero id",
function() {
const room = new Room(roomId, null, userA);
room.setSummary({'m.heroes': [userB]});
expect(room.guessDMUserId()).toEqual(userB);
});
it("should return first member that isn't self",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true,
})]);
expect(room.guessDMUserId()).toEqual(userB);
});
it("should return self if only member present",
function() {
const room = new Room(roomId, null, userA);
expect(room.guessDMUserId()).toEqual(userA);
});
});
describe("maySendMessage", function() {
it("should return false if synced membership not join",
function() {
const room = new Room(roomId, null, userA);
room.updateMyMembership("invite");
expect(room.maySendMessage()).toEqual(false);
room.updateMyMembership("leave");
expect(room.maySendMessage()).toEqual(false);
room.updateMyMembership("join");
expect(room.maySendMessage()).toEqual(true);
});
});
});
+25 -16
View File
@@ -1,13 +1,18 @@
// This file had a function whose name is all caps, which displeases eslint
/* eslint new-cap: "off" */
const q = require("q");
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const MatrixScheduler = sdk.MatrixScheduler;
const MatrixError = sdk.MatrixError;
const utils = require("../test-utils");
import expect from 'expect';
import lolex from 'lolex';
describe("MatrixScheduler", function() {
let clock;
let scheduler;
let retryFn;
let queueFn;
@@ -22,7 +27,7 @@ describe("MatrixScheduler", function() {
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
jasmine.Clock.useMock();
clock = lolex.install();
scheduler = new MatrixScheduler(function(ev, attempts, err) {
if (retryFn) {
return retryFn(ev, attempts, err);
@@ -36,7 +41,11 @@ describe("MatrixScheduler", function() {
});
retryFn = null;
queueFn = null;
defer = q.defer();
defer = Promise.defer();
});
afterEach(function() {
clock.uninstall();
});
it("should process events in a queue in a FIFO manner", function(done) {
@@ -46,8 +55,8 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return "one_big_queue";
};
const deferA = q.defer();
const deferB = q.defer();
const deferA = Promise.defer();
const deferB = Promise.defer();
let resolvedA = false;
scheduler.setProcessFunction(function(event) {
if (resolvedA) {
@@ -71,7 +80,7 @@ describe("MatrixScheduler", function() {
it("should invoke the retryFn on failure and wait the amount of time specified",
function(done) {
const waitTimeMs = 1500;
const retryDefer = q.defer();
const retryDefer = Promise.defer();
retryFn = function() {
retryDefer.resolve();
return waitTimeMs;
@@ -88,7 +97,7 @@ describe("MatrixScheduler", function() {
return defer.promise;
} else if (procCount === 2) {
// don't care about this defer
return q.defer().promise;
return Promise.defer().promise;
}
expect(procCount).toBeLessThan(3);
});
@@ -98,7 +107,7 @@ describe("MatrixScheduler", function() {
defer.reject({});
retryDefer.promise.done(function() {
expect(procCount).toEqual(1);
jasmine.Clock.tick(waitTimeMs);
clock.tick(waitTimeMs);
expect(procCount).toEqual(2);
done();
});
@@ -116,8 +125,8 @@ describe("MatrixScheduler", function() {
return "yep";
};
const deferA = q.defer();
const deferB = q.defer();
const deferA = Promise.defer();
const deferB = Promise.defer();
let procCount = 0;
scheduler.setProcessFunction(function(ev) {
procCount += 1;
@@ -168,7 +177,7 @@ describe("MatrixScheduler", function() {
const expectOrder = [
eventA.getId(), eventB.getId(), eventD.getId(),
];
const deferA = q.defer();
const deferA = Promise.defer();
scheduler.setProcessFunction(function(event) {
const id = expectOrder.shift();
expect(id).toEqual(event.getId());
@@ -186,7 +195,7 @@ describe("MatrixScheduler", function() {
setTimeout(function() {
deferA.resolve({});
}, 1000);
jasmine.Clock.tick(1000);
clock.tick(1000);
});
describe("queueEvent", function() {
@@ -202,8 +211,8 @@ describe("MatrixScheduler", function() {
return "yep";
};
const prom = scheduler.queueEvent(eventA);
expect(prom).toBeDefined();
expect(prom.then).toBeDefined();
expect(prom).toBeTruthy();
expect(prom.then).toBeTruthy();
});
});
@@ -212,14 +221,14 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return null;
};
expect(scheduler.getQueueForEvent(eventA)).toBeNull();
expect(scheduler.getQueueForEvent(eventA)).toBe(null);
});
it("should return null if the mapped queue doesn't exist", function() {
queueFn = function() {
return "yep";
};
expect(scheduler.getQueueForEvent(eventA)).toBeNull();
expect(scheduler.getQueueForEvent(eventA)).toBe(null);
});
it("should return a list of events in the queue and modifications to" +
+413
View File
@@ -0,0 +1,413 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import 'source-map-support/register';
import utils from "../test-utils";
import sdk from "../..";
import expect from 'expect';
const SyncAccumulator = sdk.SyncAccumulator;
describe("SyncAccumulator", function() {
let sa;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
sa = new SyncAccumulator({
maxTimelineEntries: 10,
});
});
it("should return the same /sync response if accumulated exactly once", () => {
// technically cheating since we also cheekily pre-populate keys we
// know that the sync accumulator will pre-populate.
// It isn't 100% transitive.
const res = {
next_batch: "abc",
rooms: {
invite: {},
leave: {},
join: {
"!foo:bar": {
account_data: { events: [] },
ephemeral: { events: [] },
unread_notifications: {},
state: {
events: [
member("alice", "join"),
member("bob", "join"),
],
},
summary: {
"m.heroes": undefined,
"m.joined_member_count": undefined,
"m.invited_member_count": undefined,
},
timeline: {
events: [msg("alice", "hi")],
prev_batch: "something",
},
},
},
},
};
sa.accumulate(res);
const output = sa.getJSON();
expect(output.nextBatch).toEqual(res.next_batch);
expect(output.roomsData).toEqual(res.rooms);
});
it("should prune the timeline to the oldest prev_batch within the limit", () => {
// maxTimelineEntries is 10 so we should get back all
// 10 timeline messages with a prev_batch of "pinned_to_1"
sa.accumulate(syncSkeleton({
state: { events: [member("alice", "join")] },
timeline: {
events: [
msg("alice", "1"),
msg("alice", "2"),
msg("alice", "3"),
msg("alice", "4"),
msg("alice", "5"),
msg("alice", "6"),
msg("alice", "7"),
],
prev_batch: "pinned_to_1",
},
}));
sa.accumulate(syncSkeleton({
state: { events: [] },
timeline: {
events: [
msg("alice", "8"),
],
prev_batch: "pinned_to_8",
},
}));
sa.accumulate(syncSkeleton({
state: { events: [] },
timeline: {
events: [
msg("alice", "9"),
msg("alice", "10"),
],
prev_batch: "pinned_to_10",
},
}));
let output = sa.getJSON().roomsData.join["!foo:bar"];
expect(output.timeline.events.length).toEqual(10);
output.timeline.events.forEach((e, i) => {
expect(e.content.body).toEqual(""+(i+1));
});
expect(output.timeline.prev_batch).toEqual("pinned_to_1");
// accumulate more messages. Now it can't have a prev_batch of "pinned to 1"
// AND give us <= 10 messages without losing messages in-between.
// It should try to find the oldest prev_batch which still fits into 10
// messages, which is "pinned to 8".
sa.accumulate(syncSkeleton({
state: { events: [] },
timeline: {
events: [
msg("alice", "11"),
msg("alice", "12"),
msg("alice", "13"),
msg("alice", "14"),
msg("alice", "15"),
msg("alice", "16"),
msg("alice", "17"),
],
prev_batch: "pinned_to_11",
},
}));
output = sa.getJSON().roomsData.join["!foo:bar"];
expect(output.timeline.events.length).toEqual(10);
output.timeline.events.forEach((e, i) => {
expect(e.content.body).toEqual(""+(i+8));
});
expect(output.timeline.prev_batch).toEqual("pinned_to_8");
});
it("should remove the stored timeline on limited syncs", () => {
sa.accumulate(syncSkeleton({
state: { events: [member("alice", "join")] },
timeline: {
events: [
msg("alice", "1"),
msg("alice", "2"),
msg("alice", "3"),
],
prev_batch: "pinned_to_1",
},
}));
// some time passes and now we get a limited sync
sa.accumulate(syncSkeleton({
state: { events: [] },
timeline: {
limited: true,
events: [
msg("alice", "51"),
msg("alice", "52"),
msg("alice", "53"),
],
prev_batch: "pinned_to_51",
},
}));
const output = sa.getJSON().roomsData.join["!foo:bar"];
expect(output.timeline.events.length).toEqual(3);
output.timeline.events.forEach((e, i) => {
expect(e.content.body).toEqual(""+(i+51));
});
expect(output.timeline.prev_batch).toEqual("pinned_to_51");
});
it("should drop typing notifications", () => {
const res = syncSkeleton({
ephemeral: {
events: [{
type: "m.typing",
content: {
user_ids: ["@alice:localhost"],
},
room_id: "!foo:bar",
}],
},
});
sa.accumulate(res);
expect(
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events.length,
).toEqual(0);
});
it("should clobber account data based on event type", () => {
const acc1 = {
type: "favourite.food",
content: {
food: "banana",
},
};
const acc2 = {
type: "favourite.food",
content: {
food: "apple",
},
};
sa.accumulate(syncSkeleton({
account_data: {
events: [acc1],
},
}));
sa.accumulate(syncSkeleton({
account_data: {
events: [acc2],
},
}));
expect(
sa.getJSON().roomsData.join["!foo:bar"].account_data.events.length,
).toEqual(1);
expect(
sa.getJSON().roomsData.join["!foo:bar"].account_data.events[0],
).toEqual(acc2);
});
it("should clobber global account data based on event type", () => {
const acc1 = {
type: "favourite.food",
content: {
food: "banana",
},
};
const acc2 = {
type: "favourite.food",
content: {
food: "apple",
},
};
sa.accumulate({
account_data: {
events: [acc1],
},
});
sa.accumulate({
account_data: {
events: [acc2],
},
});
expect(
sa.getJSON().accountData.length,
).toEqual(1);
expect(
sa.getJSON().accountData[0],
).toEqual(acc2);
});
it("should accumulate read receipts", () => {
const receipt1 = {
type: "m.receipt",
room_id: "!foo:bar",
content: {
"$event1:localhost": {
"m.read": {
"@alice:localhost": { ts: 1 },
"@bob:localhost": { ts: 2 },
},
"some.other.receipt.type": {
"@should_be_ignored:localhost": { key: "val" },
},
},
},
};
const receipt2 = {
type: "m.receipt",
room_id: "!foo:bar",
content: {
"$event2:localhost": {
"m.read": {
"@bob:localhost": { ts: 2 }, // clobbers event1 receipt
"@charlie:localhost": { ts: 3 },
},
},
},
};
sa.accumulate(syncSkeleton({
ephemeral: {
events: [receipt1],
},
}));
sa.accumulate(syncSkeleton({
ephemeral: {
events: [receipt2],
},
}));
expect(
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events.length,
).toEqual(1);
expect(
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events[0],
).toEqual({
type: "m.receipt",
room_id: "!foo:bar",
content: {
"$event1:localhost": {
"m.read": {
"@alice:localhost": { ts: 1 },
},
},
"$event2:localhost": {
"m.read": {
"@bob:localhost": { ts: 2 },
"@charlie:localhost": { ts: 3 },
},
},
},
});
});
describe("summary field", function() {
function createSyncResponseWithSummary(summary) {
return {
next_batch: "abc",
rooms: {
invite: {},
leave: {},
join: {
"!foo:bar": {
account_data: { events: [] },
ephemeral: { events: [] },
unread_notifications: {},
state: {
events: [],
},
summary: summary,
timeline: {
events: [],
prev_batch: "something",
},
},
},
},
};
}
it("should copy summary properties", function() {
sa.accumulate(createSyncResponseWithSummary({
"m.heroes": ["@alice:bar"],
"m.invited_member_count": 2,
}));
const summary = sa.getJSON().roomsData.join["!foo:bar"].summary;
expect(summary["m.invited_member_count"]).toEqual(2);
expect(summary["m.heroes"]).toEqual(["@alice:bar"]);
});
it("should accumulate summary properties", function() {
sa.accumulate(createSyncResponseWithSummary({
"m.heroes": ["@alice:bar"],
"m.invited_member_count": 2,
}));
sa.accumulate(createSyncResponseWithSummary({
"m.heroes": ["@bob:bar"],
"m.joined_member_count": 5,
}));
const summary = sa.getJSON().roomsData.join["!foo:bar"].summary;
expect(summary["m.invited_member_count"]).toEqual(2);
expect(summary["m.joined_member_count"]).toEqual(5);
expect(summary["m.heroes"]).toEqual(["@bob:bar"]);
});
});
});
function syncSkeleton(joinObj) {
joinObj = joinObj || {};
return {
next_batch: "abc",
rooms: {
join: {
"!foo:bar": joinObj,
},
},
};
}
function msg(localpart, text) {
return {
content: {
body: text,
},
origin_server_ts: 123456789,
sender: "@" + localpart + ":localhost",
type: "m.room.message",
};
}
function member(localpart, membership) {
return {
content: {
membership: membership,
},
origin_server_ts: 123456789,
state_key: "@" + localpart + ":localhost",
sender: "@" + localpart + ":localhost",
type: "m.room.member",
};
}
+18 -16
View File
@@ -1,11 +1,13 @@
"use strict";
const q = require("q");
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const EventTimeline = sdk.EventTimeline;
const TimelineWindow = sdk.TimelineWindow;
const TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
const utils = require("../test-utils");
import expect from 'expect';
const ROOM_ID = "roomId";
const USER_ID = "userId";
@@ -155,7 +157,7 @@ describe("TimelineWindow", function() {
client = {};
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
return q(timeline);
return Promise.resolve(timeline);
};
return new TimelineWindow(client, timelineSet, opts);
@@ -177,7 +179,7 @@ describe("TimelineWindow", function() {
timelineWindow.load(undefined, 2).then(function() {
const expectedEvents = liveTimeline.getEvents().slice(1);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("should initialise from a specific event", function(done) {
@@ -189,14 +191,14 @@ describe("TimelineWindow", function() {
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
expect(eventId0).toEqual(eventId);
return q(timeline);
return Promise.resolve(timeline);
};
const timelineWindow = new TimelineWindow(client, timelineSet);
timelineWindow.load(eventId, 3).then(function() {
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("canPaginate should return false until load has returned",
@@ -217,7 +219,7 @@ describe("TimelineWindow", function() {
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return q(timeline);
return Promise.resolve(timeline);
};
timelineWindow.load(eventId, 3).then(function() {
@@ -227,7 +229,7 @@ describe("TimelineWindow", function() {
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
});
@@ -275,7 +277,7 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("should advance into next timeline", function(done) {
@@ -320,7 +322,7 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("should retreat into previous timeline", function(done) {
@@ -365,7 +367,7 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("should make forward pagination requests", function(done) {
@@ -381,7 +383,7 @@ describe("TimelineWindow", function() {
expect(opts.limit).toEqual(2);
addEventsToTimeline(timeline, 3, false);
return q(true);
return Promise.resolve(true);
};
timelineWindow.load(eventId, 3).then(function() {
@@ -397,7 +399,7 @@ describe("TimelineWindow", function() {
expect(success).toBe(true);
const expectedEvents = timeline.getEvents().slice(0, 5);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
@@ -414,7 +416,7 @@ describe("TimelineWindow", function() {
expect(opts.limit).toEqual(2);
addEventsToTimeline(timeline, 3, true);
return q(true);
return Promise.resolve(true);
};
timelineWindow.load(eventId, 3).then(function() {
@@ -430,7 +432,7 @@ describe("TimelineWindow", function() {
expect(success).toBe(true);
const expectedEvents = timeline.getEvents().slice(1, 6);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("should limit the number of unsuccessful pagination requests",
@@ -447,7 +449,7 @@ describe("TimelineWindow", function() {
expect(opts.backwards).toBe(false);
expect(opts.limit).toEqual(2);
paginateCount += 1;
return q(true);
return Promise.resolve(true);
};
timelineWindow.load(eventId, 3).then(function() {
@@ -469,7 +471,7 @@ describe("TimelineWindow", function() {
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
});
});
+3
View File
@@ -1,8 +1,11 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const User = sdk.User;
const utils = require("../test-utils");
import expect from 'expect';
describe("User", function() {
const userId = "@alice:bar";
let user;
+5 -2
View File
@@ -1,7 +1,10 @@
"use strict";
import 'source-map-support/register';
const utils = require("../../lib/utils");
const testUtils = require("../test-utils");
import expect from 'expect';
describe("utils", function() {
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
@@ -132,7 +135,7 @@ describe("utils", function() {
utils.checkObjectHasKeys({
foo: "bar",
}, ["foo"]);
}).not.toThrow();
}).toNotThrow();
});
});
@@ -149,7 +152,7 @@ describe("utils", function() {
utils.checkObjectHasNoAdditionalKeys({
foo: "bar",
}, ["foo"]);
}).not.toThrow();
}).toNotThrow();
});
});
+46
View File
@@ -0,0 +1,46 @@
/*
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 default 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) {
for (const eventName of eventNames) {
if (this.boundHandlers[eventName] === undefined) {
this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName);
}
const boundHandler = this.boundHandlers[eventName];
source.on(eventName, boundHandler);
}
}
}
+585 -15
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -53,6 +54,8 @@ const utils = require("./utils");
* to all requests with this client. Useful for application services which require
* <code>?user_id=</code>.
*
* @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
* Authorization header instead of query param to send the access token to the server.
*/
function MatrixBaseApis(opts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request"]);
@@ -69,6 +72,7 @@ function MatrixBaseApis(opts) {
onlyData: true,
extraParams: opts.queryParams,
localTimeoutMs: opts.localTimeoutMs,
useAuthorizationHeader: opts.useAuthorizationHeader,
};
this._http = new httpApi.MatrixHttpApi(this, httpOpts);
@@ -85,9 +89,14 @@ MatrixBaseApis.prototype.getHomeserverUrl = function() {
/**
* Get the Identity Server URL of this client
* @param {boolean} stripProto whether or not to strip the protocol from the URL
* @return {string} Identity Server URL of this client
*/
MatrixBaseApis.prototype.getIdentityServerUrl = function() {
MatrixBaseApis.prototype.getIdentityServerUrl = function(stripProto=false) {
if (stripProto && (this.idBaseUrl.startsWith("http://") ||
this.idBaseUrl.startsWith("https://"))) {
return this.idBaseUrl.split("://")[1];
}
return this.idBaseUrl;
};
@@ -119,12 +128,28 @@ MatrixBaseApis.prototype.makeTxnId = function() {
// Registration/Login operations
// =============================
/**
* Check whether a username is available prior to registration. An error response
* indicates an invalid/unavailable username.
* @param {string} username The username to check the availability of.
* @return {module:client.Promise} Resolves: to `true`.
*/
MatrixBaseApis.prototype.isUsernameAvailable = function(username) {
return this._http.authedRequest(
undefined, "GET", '/register/available', { username: username },
).then((response) => {
return response.available;
});
};
/**
* @param {string} username
* @param {string} password
* @param {string} sessionId
* @param {Object} auth
* @param {boolean} bindEmail
* @param {Object} bindThreepids Set key 'email' to true to bind any email
* threepid uses during registration in the ID server. Set 'msisdn' to
* true to bind msisdn.
* @param {string} guestAccessToken
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
@@ -132,10 +157,17 @@ MatrixBaseApis.prototype.makeTxnId = function() {
*/
MatrixBaseApis.prototype.register = function(
username, password,
sessionId, auth, bindEmail, guestAccessToken,
sessionId, auth, bindThreepids, guestAccessToken,
callback,
) {
if (auth === undefined) {
// backwards compat
if (bindThreepids === true) {
bindThreepids = {email: true};
} else if (bindThreepids === null || bindThreepids === undefined) {
bindThreepids = {};
}
if (auth === undefined || auth === null) {
auth = {};
}
if (sessionId) {
@@ -151,12 +183,24 @@ MatrixBaseApis.prototype.register = function(
if (password !== undefined && password !== null) {
params.password = password;
}
if (bindEmail !== undefined && bindEmail !== null) {
params.bind_email = bindEmail;
if (bindThreepids.email) {
params.bind_email = true;
}
if (bindThreepids.msisdn) {
params.bind_msisdn = true;
}
if (guestAccessToken !== undefined && guestAccessToken !== null) {
params.guest_access_token = guestAccessToken;
}
// Temporary parameter added to make the register endpoint advertise
// msisdn flows. This exists because there are clients that break
// when given stages they don't recognise. This parameter will cease
// to be necessary once these old clients are gone.
// Only send it if we send any params at all (the password param is
// mandatory, so if we send any params, we'll send the password param)
if (password !== undefined && password !== null) {
params.x_show_msisdn = true;
}
return this.registerRequest(params, undefined, callback);
};
@@ -294,18 +338,29 @@ MatrixBaseApis.prototype.logout = function(callback) {
* it is up to the caller to either reset or destroy the MatrixClient after
* this method succeeds.
* @param {object} auth Optional. Auth data to supply for User-Interactive auth.
* @param {module:client.callback} callback Optional.
* @param {boolean} erase Optional. If set, send as `erase` attribute in the
* JSON request body, indicating whether the account should be erased. Defaults
* to false.
* @return {module:client.Promise} Resolves: On success, the empty object
*/
MatrixBaseApis.prototype.deactivateAccount = function(auth, callback) {
let body = {};
if (auth) {
body = {
auth: auth,
};
MatrixBaseApis.prototype.deactivateAccount = function(auth, erase) {
if (typeof(erase) === 'function') {
throw new Error(
'deactivateAccount no longer accepts a callback parameter',
);
}
const body = {};
if (auth) {
body.auth = auth;
}
if (erase !== undefined) {
body.erase = erase;
}
return this._http.authedRequestWithPrefix(
callback, "POST", '/account/deactivate', undefined, body, httpApi.PREFIX_UNSTABLE,
undefined, "POST", '/account/deactivate', undefined, body,
httpApi.PREFIX_R0,
);
};
@@ -362,6 +417,402 @@ MatrixBaseApis.prototype.roomState = function(roomId, callback) {
return this._http.authedRequest(callback, "GET", path);
};
/**
* Get an event in a room by its event id.
* @param {string} roomId
* @param {string} eventId
* @param {module:client.callback} callback Optional.
*
* @return {Promise} Resolves to an object containing the event.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.fetchRoomEvent = function(roomId, eventId, callback) {
const path = utils.encodeUri(
"/rooms/$roomId/event/$eventId", {
$roomId: roomId,
$eventId: eventId,
},
);
return this._http.authedRequest(callback, "GET", path);
};
/**
* @param {string} roomId
* @param {string} includeMembership the membership type to include in the response
* @param {string} excludeMembership the membership type to exclude from the response
* @param {string} atEventId the id of the event for which moment in the timeline the members should be returned for
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: dictionary of userid to profile information
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.members =
function(roomId, includeMembership, excludeMembership, atEventId, callback) {
const queryParams = {};
if (includeMembership) {
queryParams.membership = includeMembership;
}
if (excludeMembership) {
queryParams.not_membership = excludeMembership;
}
if (atEventId) {
queryParams.at = atEventId;
}
const queryString = utils.encodeParams(queryParams);
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString,
{$roomId: roomId});
return this._http.authedRequest(callback, "GET", path);
};
/**
* Upgrades a room to a new protocol version
* @param {string} roomId
* @param {string} newVersion The target version to upgrade to
* @return {module:client.Promise} Resolves: Object with key 'replacement_room'
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.upgradeRoom = function(roomId, newVersion) {
const path = utils.encodeUri("/rooms/$roomId/upgrade", {$roomId: roomId});
return this._http.authedRequest(
undefined, "POST", path, undefined, {new_version: newVersion},
);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group summary object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupSummary = function(groupId) {
const path = utils.encodeUri("/groups/$groupId/summary", {$groupId: groupId});
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group profile object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupProfile = function(groupId) {
const path = utils.encodeUri("/groups/$groupId/profile", {$groupId: groupId});
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {string} groupId
* @param {Object} profile The group profile object
* @param {string=} profile.name Name of the group
* @param {string=} profile.avatar_url MXC avatar URL
* @param {string=} profile.short_description A short description of the room
* @param {string=} profile.long_description A longer HTML description of the room
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setGroupProfile = function(groupId, profile) {
const path = utils.encodeUri("/groups/$groupId/profile", {$groupId: groupId});
return this._http.authedRequest(
undefined, "POST", path, undefined, profile,
);
};
/**
* @param {string} groupId
* @param {object} policy The join policy for the group. Must include at
* least a 'type' field which is 'open' if anyone can join the group
* the group without prior approval, or 'invite' if an invite is
* required to join.
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setGroupJoinPolicy = function(groupId, policy) {
const path = utils.encodeUri(
"/groups/$groupId/settings/m.join_policy",
{$groupId: groupId},
);
return this._http.authedRequest(
undefined, "PUT", path, undefined, {
'm.join_policy': policy,
},
);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group users list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupUsers = function(groupId) {
const path = utils.encodeUri("/groups/$groupId/users", {$groupId: groupId});
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group users list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupInvitedUsers = function(groupId) {
const path = utils.encodeUri("/groups/$groupId/invited_users", {$groupId: groupId});
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group rooms list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupRooms = function(groupId) {
const path = utils.encodeUri("/groups/$groupId/rooms", {$groupId: groupId});
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {string} groupId
* @param {string} userId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.inviteUserToGroup = function(groupId, userId) {
const path = utils.encodeUri(
"/groups/$groupId/admin/users/invite/$userId",
{$groupId: groupId, $userId: userId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @param {string} groupId
* @param {string} userId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.removeUserFromGroup = function(groupId, userId) {
const path = utils.encodeUri(
"/groups/$groupId/admin/users/remove/$userId",
{$groupId: groupId, $userId: userId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @param {string} groupId
* @param {string} userId
* @param {string} roleId Optional.
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addUserToGroupSummary = function(groupId, userId, roleId) {
const path = utils.encodeUri(
roleId ?
"/groups/$groupId/summary/$roleId/users/$userId" :
"/groups/$groupId/summary/users/$userId",
{$groupId: groupId, $roleId: roleId, $userId: userId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @param {string} groupId
* @param {string} userId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.removeUserFromGroupSummary = function(groupId, userId) {
const path = utils.encodeUri(
"/groups/$groupId/summary/users/$userId",
{$groupId: groupId, $userId: userId},
);
return this._http.authedRequest(undefined, "DELETE", path, undefined, {});
};
/**
* @param {string} groupId
* @param {string} roomId
* @param {string} categoryId Optional.
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addRoomToGroupSummary = function(groupId, roomId, categoryId) {
const path = utils.encodeUri(
categoryId ?
"/groups/$groupId/summary/$categoryId/rooms/$roomId" :
"/groups/$groupId/summary/rooms/$roomId",
{$groupId: groupId, $categoryId: categoryId, $roomId: roomId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @param {string} groupId
* @param {string} roomId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.removeRoomFromGroupSummary = function(groupId, roomId) {
const path = utils.encodeUri(
"/groups/$groupId/summary/rooms/$roomId",
{$groupId: groupId, $roomId: roomId},
);
return this._http.authedRequest(undefined, "DELETE", path, undefined, {});
};
/**
* @param {string} groupId
* @param {string} roomId
* @param {bool} isPublic Whether the room-group association is visible to non-members
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addRoomToGroup = function(groupId, roomId, isPublic) {
if (isPublic === undefined) {
isPublic = true;
}
const path = utils.encodeUri(
"/groups/$groupId/admin/rooms/$roomId",
{$groupId: groupId, $roomId: roomId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined,
{ "m.visibility": { type: isPublic ? "public" : "private" } },
);
};
/**
* Configure the visibility of a room-group association.
* @param {string} groupId
* @param {string} roomId
* @param {bool} isPublic Whether the room-group association is visible to non-members
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.updateGroupRoomVisibility = function(groupId, roomId, isPublic) {
// NB: The /config API is generic but there's not much point in exposing this yet as synapse
// is the only server to implement this. In future we should consider an API that allows
// arbitrary configuration, i.e. "config/$configKey".
const path = utils.encodeUri(
"/groups/$groupId/admin/rooms/$roomId/config/m.visibility",
{$groupId: groupId, $roomId: roomId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined,
{ type: isPublic ? "public" : "private" },
);
};
/**
* @param {string} groupId
* @param {string} roomId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.removeRoomFromGroup = function(groupId, roomId) {
const path = utils.encodeUri(
"/groups/$groupId/admin/rooms/$roomId",
{$groupId: groupId, $roomId: roomId},
);
return this._http.authedRequest(undefined, "DELETE", path, undefined, {});
};
/**
* @param {string} groupId
* @param {Object} opts Additional options to send alongside the acceptance.
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.acceptGroupInvite = function(groupId, opts = null) {
const path = utils.encodeUri(
"/groups/$groupId/self/accept_invite",
{$groupId: groupId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, opts || {});
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.joinGroup = function(groupId) {
const path = utils.encodeUri(
"/groups/$groupId/self/join",
{$groupId: groupId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.leaveGroup = function(groupId) {
const path = utils.encodeUri(
"/groups/$groupId/self/leave",
{$groupId: groupId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @return {module:client.Promise} Resolves: The groups to which the user is joined
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getJoinedGroups = function() {
const path = utils.encodeUri("/joined_groups");
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {Object} content Request content
* @param {string} content.localpart The local part of the desired group ID
* @param {Object} content.profile Group profile object
* @return {module:client.Promise} Resolves: Object with key group_id: id of the created group
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.createGroup = function(content) {
const path = utils.encodeUri("/create_group");
return this._http.authedRequest(
undefined, "POST", path, undefined, content,
);
};
/**
* @param {string[]} userIds List of user IDs
* @return {module:client.Promise} Resolves: Object as exmaple below
*
* {
* "users": {
* "@bob:example.com": {
* "+example:example.com"
* }
* }
* }
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getPublicisedGroups = function(userIds) {
const path = utils.encodeUri("/publicised_groups");
return this._http.authedRequest(
undefined, "POST", path, undefined, { user_ids: userIds },
);
};
/**
* @param {string} groupId
* @param {bool} isPublic Whether the user's membership of this group is made public
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setGroupPublicity = function(groupId, isPublic) {
const path = utils.encodeUri(
"/groups/$groupId/self/update_publicity",
{$groupId: groupId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {
publicise: isPublic,
});
};
/**
* Retrieve a state event.
* @param {string} roomId
@@ -448,6 +899,34 @@ MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
);
};
/**
* Set a marker to indicate the point in a room before which the user has read every
* event. This can be retrieved from room account data (the event type is `m.fully_read`)
* and displayed as a horizontal line in the timeline that is visually distinct to the
* position of the user's own read receipt.
* @param {string} roomId ID of the room that has been read
* @param {string} rmEventId ID of the event that has been read
* @param {string} rrEventId ID of the event tracked by the read receipt. This is here
* for convenience because the RR and the RM are commonly updated at the same time as
* each other. Optional.
* @return {module:client.Promise} Resolves: the empty object, {}.
*/
MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
function(roomId, rmEventId, rrEventId) {
const path = utils.encodeUri("/rooms/$roomId/read_markers", {
$roomId: roomId,
});
const content = {
"m.fully_read": rmEventId,
"m.read": rrEventId,
};
return this._http.authedRequest(
undefined, "POST", path, undefined, content,
);
};
// Room Directory operations
// =========================
@@ -614,6 +1093,31 @@ MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService =
);
};
// User Directory Operations
// =========================
/**
* Query the user directory with a term matching user IDs, display names and domains.
* @param {object} opts options
* @param {string} opts.term the term with which to search.
* @param {number} opts.limit the maximum number of results to return. The server will
* apply a limit if unspecified.
* @return {module:client.Promise} Resolves: an array of results.
*/
MatrixBaseApis.prototype.searchUserDirectory = function(opts) {
const body = {
search_term: opts.term,
};
if (opts.limit !== undefined) {
body.limit = opts.limit;
}
return this._http.authedRequest(
undefined, "POST", "/user_directory/search", undefined, body,
);
};
// Media operations
// ================
@@ -630,6 +1134,10 @@ MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService =
* @param {string=} opts.name Name to give the file on the server. Defaults
* to <tt>file.name</tt>.
*
* @param {boolean=} opts.includeFilename if false will not send the filename,
* e.g for encrypted file uploads where filename leaks are undesirable.
* Defaults to true.
*
* @param {string=} opts.type Content-type for the upload. Defaults to
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
*
@@ -646,6 +1154,10 @@ MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService =
* invoke on success/failure. See the promise return values for more
* information.
*
* @param {Function=} opts.progressHandler Optional. Called when a chunk of
* data has been uploaded, with an object containing the fields `loaded`
* (number of bytes transferred) and `total` (total size, if known).
*
* @return {module:client.Promise} Resolves to response object, as
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
@@ -835,6 +1347,27 @@ MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
);
};
/**
* Delete multiple device
*
* @param {string[]} devices IDs of the devices to delete
* @param {object} auth Optional. Auth data to supply for User-Interactive auth.
* @return {module:client.Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.deleteMultipleDevices = function(devices, auth) {
const body = {devices};
if (auth) {
body.auth = auth;
}
return this._http.authedRequestWithPrefix(
undefined, "POST", "/delete_devices", undefined, body,
httpApi.PREFIX_UNSTABLE,
);
};
// Push operations
// ===============
@@ -1142,6 +1675,35 @@ MatrixBaseApis.prototype.requestEmailToken = function(email, clientSecret,
);
};
/**
* Submits an MSISDN token to the identity server
*
* This is used when submitting the code sent by SMS to a phone number.
* The ID server has an equivalent API for email but the js-sdk does
* not expose this, since email is normally validated by the user clicking
* a link rather than entering a code.
*
* @param {string} sid The sid given in the response to requestToken
* @param {string} clientSecret A secret binary string generated by the client.
* This must be the same value submitted in the requestToken call.
* @param {string} token The token, as enetered by the user.
*
* @return {module:client.Promise} Resolves: Object, currently with no parameters.
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if No ID server is set
*/
MatrixBaseApis.prototype.submitMsisdnToken = function(sid, clientSecret, token) {
const params = {
sid: sid,
client_secret: clientSecret,
token: token,
};
return this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
};
/**
* Looks up the public Matrix ID mapping for a given 3rd party
* identifier from the Identity Server
@@ -1208,7 +1770,15 @@ MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
return this._http.authedRequestWithPrefix(
undefined, "GET", "/thirdparty/protocols", undefined, undefined,
httpApi.PREFIX_UNSTABLE,
);
).then((response) => {
// sanity check
if (!response || typeof(response) !== 'object') {
throw new Error(
`/thirdparty/protocols did not return an object: ${response}`,
);
}
return response;
});
};
/**
+879 -330
View File
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
/*
Copyright 2018 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.
*/
"use strict";
/** @module ContentHelpers */
module.exports = {
/**
* Generates the content for a HTML Message event
* @param {string} body the plaintext body of the message
* @param {string} htmlBody the HTML representation of the message
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
makeHtmlMessage: function(body, htmlBody) {
return {
msgtype: "m.text",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
},
/**
* Generates the content for a HTML Notice event
* @param {string} body the plaintext body of the notice
* @param {string} htmlBody the HTML representation of the notice
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
makeHtmlNotice: function(body, htmlBody) {
return {
msgtype: "m.notice",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
},
/**
* Generates the content for a HTML Emote event
* @param {string} body the plaintext body of the emote
* @param {string} htmlBody the HTML representation of the emote
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
makeHtmlEmote: function(body, htmlBody) {
return {
msgtype: "m.emote",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
},
/**
* Generates the content for a Plaintext Message event
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
makeTextMessage: function(body) {
return {
msgtype: "m.text",
body: body,
};
},
/**
* Generates the content for a Plaintext Notice event
* @param {string} body the plaintext body of the notice
* @returns {{msgtype: string, body: string}}
*/
makeNotice: function(body) {
return {
msgtype: "m.notice",
body: body,
};
},
/**
* Generates the content for a Plaintext Emote event
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
makeEmoteMessage: function(body) {
return {
msgtype: "m.emote",
body: body,
};
},
};
+550 -133
View File
@@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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.
@@ -21,33 +22,227 @@ limitations under the License.
* Manages the list of other users' devices
*/
import q from 'q';
import Promise from 'bluebird';
import DeviceInfo from './deviceinfo';
import olmlib from './olmlib';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
/* State transition diagram for DeviceList._deviceTrackingStatus
*
* |
* stopTrackingDeviceList V
* +---------------------> NOT_TRACKED
* | |
* +<--------------------+ | startTrackingDeviceList
* | | V
* | +-------------> PENDING_DOWNLOAD <--------------------+-+
* | | ^ | | |
* | | restart download | | start download | | invalidateUserDeviceList
* | | client failed | | | |
* | | | V | |
* | +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
* | | | |
* +<-------------------+ | download successful |
* ^ V |
* +----------------------- UP_TO_DATE ------------------------+
*/
// constants for DeviceList._deviceTrackingStatus
const TRACKING_STATUS_NOT_TRACKED = 0;
const TRACKING_STATUS_PENDING_DOWNLOAD = 1;
const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2;
const TRACKING_STATUS_UP_TO_DATE = 3;
/**
* @alias module:crypto/DeviceList
*/
export default class DeviceList {
constructor(baseApis, sessionStore, olmDevice) {
this._baseApis = baseApis;
constructor(baseApis, cryptoStore, sessionStore, olmDevice) {
this._cryptoStore = cryptoStore;
this._sessionStore = sessionStore;
this._olmDevice = olmDevice;
// users with outdated device lists
// userId -> true
this._pendingUsersWithNewDevices = {};
// userId -> {
// deviceId -> {
// [device info]
// }
// }
this._devices = {};
// which users we are tracking device status for.
// userId -> TRACKING_STATUS_*
this._deviceTrackingStatus = {}; // loaded from storage in load()
// The 'next_batch' sync token at the point the data was writen,
// ie. a token representing the point immediately after the
// moment represented by the snapshot in the db.
this._syncToken = null;
this._serialiser = new DeviceListUpdateSerialiser(
baseApis, olmDevice, this,
);
// userId -> promise
this._keyDownloadsInProgressByUser = {};
this.lastKnownSyncToken = null;
// Set whenever changes are made other than setting the sync token
this._dirty = false;
// Promise resolved when device data is saved
this._savePromise = null;
// Function that resolves the save promise
this._resolveSavePromise = null;
// The time the save is scheduled for
this._savePromiseTime = null;
// The timer used to delay the save
this._saveTimer = null;
}
/**
* Download the keys for a list of users and stores the keys in the session
* store.
* Load the device tracking state from storage
*/
async load() {
let shouldDeleteSessionStore = false;
await this._cryptoStore.doTxn(
// migrate from session store if there's data there and not here
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
if (deviceData === null) {
console.log("Migrating e2e device data...");
this._devices = this._sessionStore.getAllEndToEndDevices() || {};
this._deviceTrackingStatus = (
this._sessionStore.getEndToEndDeviceTrackingStatus() || {}
);
this._syncToken = this._sessionStore.getEndToEndDeviceSyncToken();
this._cryptoStore.storeEndToEndDeviceData({
devices: this._devices,
trackingStatus: this._deviceTrackingStatus,
syncToken: this._syncToken,
}, txn);
shouldDeleteSessionStore = true;
} else {
this._devices = deviceData ? deviceData.devices : {},
this._deviceTrackingStatus = deviceData ?
deviceData.trackingStatus : {};
this._syncToken = deviceData ? deviceData.syncToken : null;
}
});
},
);
if (shouldDeleteSessionStore) {
// migrated data is now safely persisted: remove from old store
this._sessionStore.removeEndToEndDeviceData();
}
for (const u of Object.keys(this._deviceTrackingStatus)) {
// if a download was in progress when we got shut down, it isn't any more.
if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
}
}
/**
* Save the device tracking state to storage, if any changes are
* pending other than updating the sync token
*
* The actual save will be delayed by a short amount of time to
* aggregate multiple writes to the database.
*
* @param {integer} delay Time in ms before which the save actually happens.
* By default, the save is delayed for a short period in order to batch
* multiple writes, but this behaviour can be disabled by passing 0.
*
* @return {Promise<bool>} true if the data was saved, false if
* it was not (eg. because no changes were pending). The promise
* will only resolve once the data is saved, so may take some time
* to resolve.
*/
async saveIfDirty(delay) {
if (!this._dirty) return Promise.resolve(false);
// Delay saves for a bit so we can aggregate multiple saves that happen
// in quick succession (eg. when a whole room's devices are marked as known)
if (delay === undefined) delay = 500;
const targetTime = Date.now + delay;
if (this._savePromiseTime && targetTime < this._savePromiseTime) {
// There's a save scheduled but for after we would like: cancel
// it & schedule one for the time we want
clearTimeout(this._saveTimer);
this._saveTimer = null;
this._savePromiseTime = null;
// (but keep the save promise since whatever called save before
// will still want to know when the save is done)
}
let savePromise = this._savePromise;
if (savePromise === null) {
savePromise = new Promise((resolve, reject) => {
this._resolveSavePromise = resolve;
});
this._savePromise = savePromise;
}
if (this._saveTimer === null) {
const resolveSavePromise = this._resolveSavePromise;
this._savePromiseTime = targetTime;
this._saveTimer = setTimeout(() => {
console.log('Saving device tracking data at token ' + this._syncToken);
// null out savePromise now (after the delay but before the write),
// otherwise we could return the existing promise when the save has
// actually already happened. Likewise for the dirty flag.
this._savePromiseTime = null;
this._saveTimer = null;
this._savePromise = null;
this._resolveSavePromise = null;
this._dirty = false;
this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.storeEndToEndDeviceData({
devices: this._devices,
trackingStatus: this._deviceTrackingStatus,
syncToken: this._syncToken,
}, txn);
},
).then(() => {
resolveSavePromise();
});
}, delay);
}
return savePromise;
}
/**
* Gets the sync token last set with setSyncToken
*
* @return {string} The sync token
*/
getSyncToken() {
return this._syncToken;
}
/**
* Sets the sync token that the app will pass as the 'since' to the /sync
* endpoint next time it syncs.
* The sync token must always be set after any changes made as a result of
* data in that sync since setting the sync token to a newer one will mean
* those changed will not be synced from the server if a new client starts
* up with that data.
*
* @param {string} st The sync token
*/
setSyncToken(st) {
this._syncToken = st;
}
/**
* Ensures up to date keys for a list of users are stored in the session store,
* downloading and storing them if they're not (or if forceDownload is
* true).
* @param {Array} userIds The users to fetch.
* @param {bool} forceDownload Always download the keys even if cached.
*
@@ -55,29 +250,35 @@ export default class DeviceList {
* module:crypto/deviceinfo|DeviceInfo}.
*/
downloadKeys(userIds, forceDownload) {
// promises we need to wait for while the download happens
const usersToDownload = [];
const promises = [];
let needsRefresh = false;
userIds.forEach((u) => {
const trackingStatus = this._deviceTrackingStatus[u];
if (this._keyDownloadsInProgressByUser[u]) {
// just wait for the existing download to complete
// already a key download in progress/queued for this user; its results
// will be good enough for us.
console.log(
`downloadKeys: already have a download in progress for ` +
`${u}: awaiting its result`,
);
promises.push(this._keyDownloadsInProgressByUser[u]);
} else {
if (forceDownload || !this.getStoredDevicesForUser(u)) {
this.invalidateUserDeviceList(u);
}
if (this._pendingUsersWithNewDevices[u]) {
needsRefresh = true;
}
} else if (forceDownload || trackingStatus != TRACKING_STATUS_UP_TO_DATE) {
usersToDownload.push(u);
}
});
if (needsRefresh) {
promises.push(this.refreshOutdatedDeviceLists(true));
if (usersToDownload.length != 0) {
console.log("downloadKeys: downloading for", usersToDownload);
const downloadPromise = this._doKeyDownload(usersToDownload);
promises.push(downloadPromise);
}
return q.all(promises).then(() => {
if (promises.length === 0) {
console.log("downloadKeys: already have all necessary keys");
}
return Promise.all(promises).then(() => {
return this._getDevicesFromStore(userIds);
});
}
@@ -111,7 +312,7 @@ export default class DeviceList {
* managed to get a list of devices for this user yet.
*/
getStoredDevicesForUser(userId) {
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
const devs = this._devices[userId];
if (!devs) {
return null;
}
@@ -124,6 +325,18 @@ export default class DeviceList {
return res;
}
/**
* Get the stored device data for a user, in raw object form
*
* @param {string} userId the user to get data for
*
* @return {Object} deviceId->{object} devices, or undefined if
* there is no data for this user.
*/
getRawStoredDevicesForUser(userId) {
return this._devices[userId];
}
/**
* Get the stored keys for a single device
*
@@ -134,7 +347,7 @@ export default class DeviceList {
* if we don't know about this device
*/
getStoredDevice(userId, deviceId) {
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
const devs = this._devices[userId];
if (!devs || !devs[deviceId]) {
return undefined;
}
@@ -146,11 +359,11 @@ export default class DeviceList {
*
* @param {string} userId owner of the device
* @param {string} algorithm encryption algorithm
* @param {string} sender_key curve25519 key to match
* @param {string} senderKey curve25519 key to match
*
* @return {module:crypto/deviceinfo?}
*/
getDeviceByIdentityKey(userId, algorithm, sender_key) {
getDeviceByIdentityKey(userId, algorithm, senderKey) {
if (
algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM
@@ -159,7 +372,7 @@ export default class DeviceList {
return null;
}
const devices = this._sessionStore.getEndToEndDevicesForUser(userId);
const devices = this._devices[userId];
if (!devices) {
return null;
}
@@ -178,7 +391,7 @@ export default class DeviceList {
continue;
}
const deviceKey = device.keys[keyId];
if (deviceKey == sender_key) {
if (deviceKey == senderKey) {
return DeviceInfo.fromStorage(device, deviceId);
}
}
@@ -189,14 +402,26 @@ export default class DeviceList {
}
/**
* Mark the cached device list for the given user outdated.
* Replaces the list of devices for a user with the given device list
*
* This doesn't set off an update, so that several users can be batched
* together. Call refreshOutdatedDeviceLists() for that.
* @param {string} u The user ID
* @param {Object} devs New device info for user
*/
storeDevicesForUser(u, devs) {
this._devices[u] = devs;
this._dirty = true;
}
/**
* flag the given user for device-list tracking, if they are not already.
*
* This will mean that a subsequent call to refreshOutdatedDeviceLists()
* will download the device list for the user, and that subsequent calls to
* invalidateUserDeviceList will trigger more updates.
*
* @param {String} userId
*/
invalidateUserDeviceList(userId) {
startTrackingDeviceList(userId) {
// sanity-check the userId. This is mostly paranoia, but if synapse
// can't parse the userId we give it as an mxid, it 500s the whole
// request and we can never update the device lists again (because
@@ -207,140 +432,332 @@ export default class DeviceList {
if (typeof userId !== 'string') {
throw new Error('userId must be a string; was '+userId);
}
this._pendingUsersWithNewDevices[userId] = true;
if (!this._deviceTrackingStatus[userId]) {
console.log('Now tracking device list for ' + userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
// we don't yet persist the tracking status, since there may be a lot
// of calls; we save all data together once the sync is done
this._dirty = true;
}
/**
* Start device queries for any users with outdated device lists
* Mark the given user as no longer being tracked for device-list updates.
*
* We tolerate multiple concurrent device queries, but only one query per
* user.
* This won't affect any in-progress downloads, which will still go on to
* complete; it will just mean that we don't think that we have an up-to-date
* list for future calls to downloadKeys.
*
* If any users already have downloads in progress, they are ignored - they
* will be refreshed when the current download completes anyway, so
* each user with outdated device lists will be updated eventually.
*
* The returned promise resolves immediately if there are no users with
* outdated device lists, or if all users with outdated device lists already
* have a query in progress.
*
* Otherwise, a new query request is made, and the promise resolves
* once that query completes. If the query fails, the promise will reject
* if rejectOnFailure was truthy, otherwise it will still resolve.
*
* @param {Boolean?} rejectOnFailure true to make the returned promise
* reject if the device list query fails.
*
* @return {Promise}
* @param {String} userId
*/
refreshOutdatedDeviceLists(rejectOnFailure) {
const users = Object.keys(this._pendingUsersWithNewDevices).filter(
(u) => !this._keyDownloadsInProgressByUser[u],
);
stopTrackingDeviceList(userId) {
if (this._deviceTrackingStatus[userId]) {
console.log('No longer tracking device list for ' + userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED;
if (users.length === 0) {
return q();
// we don't yet persist the tracking status, since there may be a lot
// of calls; we save all data together once the sync is done
this._dirty = true;
}
}
/**
* Set all users we're currently tracking to untracked
*
* This will flag each user whose devices we are tracking as in need of an
* update.
*/
stopTrackingAllDeviceLists() {
for (const userId of Object.keys(this._deviceTrackingStatus)) {
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED;
}
this._dirty = true;
}
/**
* Mark the cached device list for the given user outdated.
*
* If we are not tracking this user's devices, we'll do nothing. Otherwise
* we flag the user as needing an update.
*
* This doesn't actually set off an update, so that several users can be
* batched together. Call refreshOutdatedDeviceLists() for that.
*
* @param {String} userId
*/
invalidateUserDeviceList(userId) {
if (this._deviceTrackingStatus[userId]) {
console.log("Marking device list outdated for", userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
// we don't yet persist the tracking status, since there may be a lot
// of calls; we save all data together once the sync is done
this._dirty = true;
}
}
/**
* If we have users who have outdated device lists, start key downloads for them
*
* @returns {Promise} which completes when the download completes; normally there
* is no need to wait for this (it's mostly for the unit tests).
*/
refreshOutdatedDeviceLists() {
this.saveIfDirty();
const usersToDownload = [];
for (const userId of Object.keys(this._deviceTrackingStatus)) {
const stat = this._deviceTrackingStatus[userId];
if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) {
usersToDownload.push(userId);
}
}
let prom = this._doKeyDownloadForUsers(users).then(() => {
users.forEach((u) => {
delete this._keyDownloadsInProgressByUser[u];
});
return this._doKeyDownload(usersToDownload);
}
// flush out any more requests that were blocked up while that
// was going on, but let the initial promise complete now.
//
this.refreshOutdatedDeviceLists().done();
/**
* Set the stored device data for a user, in raw object form
* Used only by internal class DeviceListUpdateSerialiser
*
* @param {string} userId the user to get data for
*
* @param {Object} devices deviceId->{object} the new devices
*/
_setRawStoredDevicesForUser(userId, devices) {
this._devices[userId] = devices;
}
/**
* Fire off download update requests for the given users, and update the
* device list tracking status for them, and the
* _keyDownloadsInProgressByUser map for them.
*
* @param {String[]} users list of userIds
*
* @return {module:client.Promise} resolves when all the users listed have
* been updated. rejects if there was a problem updating any of the
* users.
*/
_doKeyDownload(users) {
if (users.length === 0) {
// nothing to do
return Promise.resolve();
}
const prom = this._serialiser.updateDevicesForUsers(
users, this._syncToken,
).then(() => {
finished(true);
}, (e) => {
console.error(
'Error updating device key cache for ' + users + ":", e,
'Error downloading keys for ' + users + ":", e,
);
// reinstate the pending flags on any users which failed; this will
// mean that we will do another download in the future, but won't
// tight-loop.
//
users.forEach((u) => {
delete this._keyDownloadsInProgressByUser[u];
this._pendingUsersWithNewDevices[u] = true;
});
// TODO: schedule a retry.
finished(false);
throw e;
});
users.forEach((u) => {
delete this._pendingUsersWithNewDevices[u];
this._keyDownloadsInProgressByUser[u] = prom;
const stat = this._deviceTrackingStatus[u];
if (stat == TRACKING_STATUS_PENDING_DOWNLOAD) {
this._deviceTrackingStatus[u] = TRACKING_STATUS_DOWNLOAD_IN_PROGRESS;
}
});
if (!rejectOnFailure) {
// normally we just want to swallow the exception - we've already
// logged it futher up.
prom = prom.catch((e) => {});
}
const finished = (success) => {
users.forEach((u) => {
this._dirty = true;
// we may have queued up another download request for this user
// since we started this request. If that happens, we should
// ignore the completion of the first one.
if (this._keyDownloadsInProgressByUser[u] !== prom) {
console.log('Another update in the queue for', u,
'- not marking up-to-date');
return;
}
delete this._keyDownloadsInProgressByUser[u];
const stat = this._deviceTrackingStatus[u];
if (stat == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
if (success) {
// we didn't get any new invalidations since this download started:
// this user's device list is now up to date.
this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE;
console.log("Device list for", u, "now up to date");
} else {
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
}
});
this.saveIfDirty();
};
return prom;
}
}
/**
* Serialises updates to device lists
*
* Ensures that results from /keys/query are not overwritten if a second call
* completes *before* an earlier one.
*
* It currently does this by ensuring only one call to /keys/query happens at a
* time (and queuing other requests up).
*/
class DeviceListUpdateSerialiser {
/*
* @param {object} baseApis Base API object
* @param {object} olmDevice The Olm Device
* @param {object} deviceList The device list object
*/
constructor(baseApis, olmDevice, deviceList) {
this._baseApis = baseApis;
this._olmDevice = olmDevice;
this._deviceList = deviceList; // the device list to be updated
this._downloadInProgress = false;
// users which are queued for download
// userId -> true
this._keyDownloadsQueuedByUser = {};
// deferred which is resolved when the queued users are downloaded.
//
// non-null indicates that we have users queued for download.
this._queuedQueryDeferred = null;
this._syncToken = null; // The sync token we send with the requests
}
/**
* @param {string[]} downloadUsers list of userIds
* Make a key query request for the given users
*
* @return {Promise}
* @param {String[]} users list of user ids
*
* @param {String} syncToken sync token to pass in the query request, to
* help the HS give the most recent results
*
* @return {module:client.Promise} resolves when all the users listed have
* been updated. rejects if there was a problem updating any of the
* users.
*/
_doKeyDownloadForUsers(downloadUsers) {
console.log('Starting key download for ' + downloadUsers);
updateDevicesForUsers(users, syncToken) {
users.forEach((u) => {
this._keyDownloadsQueuedByUser[u] = true;
});
const token = this.lastKnownSyncToken;
const opts = {};
if (token) {
opts.token = token;
if (!this._queuedQueryDeferred) {
this._queuedQueryDeferred = Promise.defer();
}
return this._baseApis.downloadKeysForUsers(
// We always take the new sync token and just use the latest one we've
// been given, since it just needs to be at least as recent as the
// sync response the device invalidation message arrived in
this._syncToken = syncToken;
if (this._downloadInProgress) {
// just queue up these users
console.log('Queued key download for', users);
return this._queuedQueryDeferred.promise;
}
// start a new download.
return this._doQueuedQueries();
}
_doQueuedQueries() {
if (this._downloadInProgress) {
throw new Error(
"DeviceListUpdateSerialiser._doQueuedQueries called with request active",
);
}
const downloadUsers = Object.keys(this._keyDownloadsQueuedByUser);
this._keyDownloadsQueuedByUser = {};
const deferred = this._queuedQueryDeferred;
this._queuedQueryDeferred = null;
console.log('Starting key download for', downloadUsers);
this._downloadInProgress = true;
const opts = {};
if (this._syncToken) {
opts.token = this._syncToken;
}
this._baseApis.downloadKeysForUsers(
downloadUsers, opts,
).then((res) => {
const dk = res.device_keys || {};
// do each user in a separate promise, to avoid wedging the CPU
// (https://github.com/vector-im/riot-web/issues/3158)
//
// of course we ought to do this in a web worker or similar, but
// this serves as an easy solution for now.
let prom = Promise.resolve();
for (const userId of downloadUsers) {
console.log('got keys for ' + userId + ':', dk[userId]);
// map from deviceid -> deviceinfo for this user
const userStore = {};
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (devs) {
Object.keys(devs).forEach((deviceId) => {
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
userStore[deviceId] = d;
});
}
_updateStoredDeviceKeysForUser(
this._olmDevice, userId, userStore, dk[userId] || {},
);
// update the session store
const storage = {};
Object.keys(userStore).forEach((deviceId) => {
storage[deviceId] = userStore[deviceId].toStorage();
prom = prom.delay(5).then(() => {
return this._processQueryResponseForUser(userId, dk[userId]);
});
this._sessionStore.storeEndToEndDevicesForUser(
userId, storage,
);
if (token) {
this._sessionStore.storeEndToEndDeviceSyncToken(token);
}
}
return prom;
}).done(() => {
console.log('Completed key download for ' + downloadUsers);
this._downloadInProgress = false;
deferred.resolve();
// if we have queued users, fire off another request.
if (this._queuedQueryDeferred) {
this._doQueuedQueries();
}
}, (e) => {
console.warn('Error downloading keys for ' + downloadUsers + ':', e);
this._downloadInProgress = false;
deferred.reject(e);
});
return deferred.promise;
}
async _processQueryResponseForUser(userId, response) {
console.log('got keys for ' + userId + ':', response);
// map from deviceid -> deviceinfo for this user
const userStore = {};
const devs = this._deviceList.getRawStoredDevicesForUser(userId);
if (devs) {
Object.keys(devs).forEach((deviceId) => {
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
userStore[deviceId] = d;
});
}
await _updateStoredDeviceKeysForUser(
this._olmDevice, userId, userStore, response || {},
);
// put the updates into thr object that will be returned as our results
const storage = {};
Object.keys(userStore).forEach((deviceId) => {
storage[deviceId] = userStore[deviceId].toStorage();
});
this._deviceList._setRawStoredDevicesForUser(userId, storage);
}
}
function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
userResult) {
let updated = false;
// remove any devices in the store which aren't in the response
for (var deviceId in userStore) {
for (const deviceId in userStore) {
if (!userStore.hasOwnProperty(deviceId)) {
continue;
}
@@ -353,7 +770,7 @@ function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
}
}
for (deviceId in userResult) {
for (const deviceId in userResult) {
if (!userResult.hasOwnProperty(deviceId)) {
continue;
}
@@ -373,7 +790,7 @@ function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
continue;
}
if (_storeDeviceKeys(_olmDevice, userStore, deviceResult)) {
if (await _storeDeviceKeys(_olmDevice, userStore, deviceResult)) {
updated = true;
}
}
@@ -384,9 +801,9 @@ function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
/*
* Process a device in a /query response, and add it to the userStore
*
* returns true if a change was made, else false
* returns (a promise for) true if a change was made, else false
*/
function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
if (!deviceResult.keys) {
// no keys?
return false;
@@ -398,7 +815,7 @@ function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
const signKeyId = "ed25519:" + deviceId;
const signKey = deviceResult.keys[signKeyId];
if (!signKey) {
console.log("Device " + userId + ":" + deviceId +
console.warn("Device " + userId + ":" + deviceId +
" has no ed25519 key");
return false;
}
@@ -406,10 +823,10 @@ function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
const unsigned = deviceResult.unsigned || {};
try {
olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
} catch (e) {
console.log("Unable to verify signature on device " +
userId + ":" + deviceId + ":", e);
console.warn("Unable to verify signature on device " +
userId + ":" + deviceId + ":" + e);
return false;
}
@@ -425,7 +842,7 @@ function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
// best off sticking with the original keys.
//
// Should we warn the user about it somehow?
console.warn("Ed25519 key for device" + userId + ": " +
console.warn("Ed25519 key for device " + userId + ":" +
deviceId + " has changed");
return false;
}
+646 -357
View File
File diff suppressed because it is too large Load Diff
+407
View File
@@ -0,0 +1,407 @@
/*
Copyright 2017 Vector Creations 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 Promise from 'bluebird';
import utils from '../utils';
/**
* Internal module. Management of outgoing room key requests.
*
* See https://docs.google.com/document/d/1m4gQkcnJkxNuBmb5NoFCIadIY-DyqqNAS3lloE73BlQ
* for draft documentation on what we're supposed to be implementing here.
*
* @module
*/
// delay between deciding we want some keys, and sending out the request, to
// allow for (a) it turning up anyway, (b) grouping requests together
const SEND_KEY_REQUESTS_DELAY_MS = 500;
/** possible states for a room key request
*
* The state machine looks like:
*
* | (cancellation sent)
* | .-------------------------------------------------.
* | | |
* V V (cancellation requested) |
* UNSENT -----------------------------+ |
* | | |
* | | |
* | (send successful) | CANCELLATION_PENDING_AND_WILL_RESEND
* V | Λ
* SENT | |
* |-------------------------------- | --------------'
* | | (cancellation requested with intent
* | | to resend the original request)
* | |
* | (cancellation requested) |
* V |
* CANCELLATION_PENDING |
* | |
* | (cancellation sent) |
* V |
* (deleted) <---------------------------+
*
* @enum {number}
*/
const ROOM_KEY_REQUEST_STATES = {
/** request not yet sent */
UNSENT: 0,
/** request sent, awaiting reply */
SENT: 1,
/** reply received, cancellation not yet sent */
CANCELLATION_PENDING: 2,
/**
* Cancellation not yet sent and will transition to UNSENT instead of
* being deleted once the cancellation has been sent.
*/
CANCELLATION_PENDING_AND_WILL_RESEND: 3,
};
export default class OutgoingRoomKeyRequestManager {
constructor(baseApis, deviceId, cryptoStore) {
this._baseApis = baseApis;
this._deviceId = deviceId;
this._cryptoStore = cryptoStore;
// handle for the delayed call to _sendOutgoingRoomKeyRequests. Non-null
// if the callback has been set, or if it is still running.
this._sendOutgoingRoomKeyRequestsTimer = null;
// sanity check to ensure that we don't end up with two concurrent runs
// of _sendOutgoingRoomKeyRequests
this._sendOutgoingRoomKeyRequestsRunning = false;
this._clientRunning = false;
}
/**
* Called when the client is started. Sets background processes running.
*/
start() {
this._clientRunning = true;
// set the timer going, to handle any requests which didn't get sent
// on the previous run of the client.
this._startTimer();
}
/**
* Called when the client is stopped. Stops any running background processes.
*/
stop() {
console.log('stopping OutgoingRoomKeyRequestManager');
// stop the timer on the next run
this._clientRunning = false;
}
/**
* Send off a room key request, if we haven't already done so.
*
* The `requestBody` is compared (with a deep-equality check) against
* previous queued or sent requests and if it matches, no change is made.
* Otherwise, a request is added to the pending list, and a job is started
* in the background to send it.
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* @param {Array<{userId: string, deviceId: string}>} recipients
*
* @returns {Promise} resolves when the request has been added to the
* pending list (or we have established that a similar request already
* exists)
*/
sendRoomKeyRequest(requestBody, recipients) {
return this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
requestBody: requestBody,
recipients: recipients,
requestId: this._baseApis.makeTxnId(),
state: ROOM_KEY_REQUEST_STATES.UNSENT,
}).then((req) => {
if (req.state === ROOM_KEY_REQUEST_STATES.UNSENT) {
this._startTimer();
}
});
}
/**
* Cancel room key requests, if any match the given requestBody
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* @param {boolean} andResend if true, transition to UNSENT instead of
* deleting after sending cancellation.
*
* @returns {Promise} resolves when the request has been updated in our
* pending list.
*/
cancelRoomKeyRequest(requestBody, andResend=false) {
return this._cryptoStore.getOutgoingRoomKeyRequest(
requestBody,
).then((req) => {
if (!req) {
// no request was made for this key
return;
}
switch (req.state) {
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING:
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
// nothing to do here
return;
case ROOM_KEY_REQUEST_STATES.UNSENT:
// just delete it
// FIXME: ghahah we may have attempted to send it, and
// not yet got a successful response. So the server
// may have seen it, so we still need to send a cancellation
// in that case :/
console.log(
'deleting unnecessary room key request for ' +
stringifyRequestBody(requestBody),
);
return this._cryptoStore.deleteOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT,
);
case ROOM_KEY_REQUEST_STATES.SENT: {
// If `andResend` is set, transition to UNSENT once the
// cancellation has successfully been sent.
const state = andResend ?
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND :
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING;
// send a cancellation.
return this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
state,
cancellationTxnId: this._baseApis.makeTxnId(),
},
).then((updatedReq) => {
if (!updatedReq) {
// updateOutgoingRoomKeyRequest couldn't find the
// request in state ROOM_KEY_REQUEST_STATES.SENT,
// so we must have raced with another tab to mark
// the request cancelled. There is no point in
// sending another cancellation since the other tab
// will do it.
console.log(
'Tried to cancel room key request for ' +
stringifyRequestBody(requestBody) +
' but it was already cancelled in another tab',
);
return;
}
// We don't want to wait for the timer, so we send it
// immediately. (We might actually end up racing with the timer,
// but that's ok: even if we make the request twice, we'll do it
// with the same transaction_id, so only one message will get
// sent).
//
// (We also don't want to wait for the response from the server
// here, as it will slow down processing of received keys if we
// do.)
this._sendOutgoingRoomKeyRequestCancellation(
updatedReq,
andResend,
).catch((e) => {
console.error(
"Error sending room key request cancellation;"
+ " will retry later.", e,
);
this._startTimer();
}).then(() => {
if (!andResend) return;
// The request has transitioned from
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
// still need to resend the request which is now UNSENT, so
// start the timer if it isn't already started.
this._startTimer();
});
});
}
default:
throw new Error('unhandled state: ' + req.state);
}
});
}
// start the background timer to send queued requests, if the timer isn't
// already running
_startTimer() {
if (this._sendOutgoingRoomKeyRequestsTimer) {
return;
}
const startSendingOutgoingRoomKeyRequests = () => {
if (this._sendOutgoingRoomKeyRequestsRunning) {
throw new Error("RoomKeyRequestSend already in progress!");
}
this._sendOutgoingRoomKeyRequestsRunning = true;
this._sendOutgoingRoomKeyRequests().finally(() => {
this._sendOutgoingRoomKeyRequestsRunning = false;
}).catch((e) => {
// this should only happen if there is an indexeddb error,
// in which case we're a bit stuffed anyway.
console.warn(
`error in OutgoingRoomKeyRequestManager: ${e}`,
);
}).done();
};
this._sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
startSendingOutgoingRoomKeyRequests,
SEND_KEY_REQUESTS_DELAY_MS,
);
}
// look for and send any queued requests. Runs itself recursively until
// there are no more requests, or there is an error (in which case, the
// timer will be restarted before the promise resolves).
_sendOutgoingRoomKeyRequests() {
if (!this._clientRunning) {
this._sendOutgoingRoomKeyRequestsTimer = null;
return Promise.resolve();
}
console.log("Looking for queued outgoing room key requests");
return this._cryptoStore.getOutgoingRoomKeyRequestByState([
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND,
ROOM_KEY_REQUEST_STATES.UNSENT,
]).then((req) => {
if (!req) {
console.log("No more outgoing room key requests");
this._sendOutgoingRoomKeyRequestsTimer = null;
return;
}
let prom;
switch (req.state) {
case ROOM_KEY_REQUEST_STATES.UNSENT:
prom = this._sendOutgoingRoomKeyRequest(req);
break;
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING:
prom = this._sendOutgoingRoomKeyRequestCancellation(req);
break;
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
prom = this._sendOutgoingRoomKeyRequestCancellation(req, true);
break;
}
return prom.then(() => {
// go around the loop again
return this._sendOutgoingRoomKeyRequests();
}).catch((e) => {
console.error("Error sending room key request; will retry later.", e);
this._sendOutgoingRoomKeyRequestsTimer = null;
this._startTimer();
}).done();
});
}
// given a RoomKeyRequest, send it and update the request record
_sendOutgoingRoomKeyRequest(req) {
console.log(
`Requesting keys for ${stringifyRequestBody(req.requestBody)}` +
` from ${stringifyRecipientList(req.recipients)}` +
`(id ${req.requestId})`,
);
const requestMessage = {
action: "request",
requesting_device_id: this._deviceId,
request_id: req.requestId,
body: req.requestBody,
};
return this._sendMessageToDevices(
requestMessage, req.recipients, req.requestId,
).then(() => {
return this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT,
{ state: ROOM_KEY_REQUEST_STATES.SENT },
);
});
}
// Given a RoomKeyRequest, cancel it and delete the request record unless
// andResend is set, in which case transition to UNSENT.
_sendOutgoingRoomKeyRequestCancellation(req, andResend) {
console.log(
`Sending cancellation for key request for ` +
`${stringifyRequestBody(req.requestBody)} to ` +
`${stringifyRecipientList(req.recipients)} ` +
`(cancellation id ${req.cancellationTxnId})`,
);
const requestMessage = {
action: "request_cancellation",
requesting_device_id: this._deviceId,
request_id: req.requestId,
};
return this._sendMessageToDevices(
requestMessage, req.recipients, req.cancellationTxnId,
).then(() => {
if (andResend) {
// We want to resend, so transition to UNSENT
return this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId,
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND,
{ state: ROOM_KEY_REQUEST_STATES.UNSENT },
);
}
return this._cryptoStore.deleteOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
);
});
}
// send a RoomKeyRequest to a list of recipients
_sendMessageToDevices(message, recipients, txnId) {
const contentMap = {};
for (const recip of recipients) {
if (!contentMap[recip.userId]) {
contentMap[recip.userId] = {};
}
contentMap[recip.userId][recip.deviceId] = message;
}
return this._baseApis.sendToDevice(
'm.room_key_request', contentMap, txnId,
);
}
}
function stringifyRequestBody(requestBody) {
// we assume that the request is for megolm keys, which are identified by
// room id and session id
return requestBody.room_id + " / " + requestBody.session_id;
}
function stringifyRecipientList(recipients) {
return '['
+ utils.map(recipients, (r) => `${r.userId}:${r.deviceId}`).join(",")
+ ']';
}
+84
View File
@@ -0,0 +1,84 @@
/*
Copyright 2018 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 crypto/RoomList
*
* Manages the list of encrypted rooms
*/
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
/**
* @alias module:crypto/RoomList
*/
export default class RoomList {
constructor(cryptoStore, sessionStore) {
this._cryptoStore = cryptoStore;
this._sessionStore = sessionStore;
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
this._roomEncryption = {};
}
async init() {
let removeSessionStoreRooms = false;
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
this._cryptoStore.getEndToEndRooms(txn, (result) => {
if (result === null || Object.keys(result).length === 0) {
// migrate from session store, if there's data there
const sessStoreRooms = this._sessionStore.getAllEndToEndRooms();
if (sessStoreRooms !== null) {
for (const roomId of Object.keys(sessStoreRooms)) {
this._cryptoStore.storeEndToEndRoom(
roomId, sessStoreRooms[roomId], txn,
);
}
}
this._roomEncryption = sessStoreRooms;
removeSessionStoreRooms = true;
} else {
this._roomEncryption = result;
}
});
},
);
if (removeSessionStoreRooms) {
this._sessionStore.removeAllEndToEndRooms();
}
}
getRoomEncryption(roomId) {
return this._roomEncryption[roomId] || null;
}
isRoomEncrypted(roomId) {
return Boolean(this.getRoomEncryption(roomId));
}
async setRoomEncryption(roomId, roomInfo) {
// important that this happens before calling into the store
// as it prevents the Crypto::setRoomEncryption from calling
// this twice for consecutive m.room.encryption events
this._roomEncryption[roomId] = roomInfo;
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
this._cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn);
},
);
}
}
+140 -97
View File
@@ -13,14 +13,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* Internal module. Defines the base classes of the encryption implementations
*
* @module crypto/algorithms/base
* @module
*/
const utils = require("../../utils");
import Promise from 'bluebird';
/**
* map of registered encryption algorithm classes. A map from string to {@link
@@ -28,7 +28,7 @@ const utils = require("../../utils");
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
*/
module.exports.ENCRYPTION_CLASSES = {};
export const ENCRYPTION_CLASSES = {};
/**
* map of registered encryption algorithm classes. Map from string to {@link
@@ -36,12 +36,11 @@ module.exports.ENCRYPTION_CLASSES = {};
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
*/
module.exports.DECRYPTION_CLASSES = {};
export const DECRYPTION_CLASSES = {};
/**
* base type for encryption implementations
*
* @constructor
* @alias module:crypto/algorithms/base.EncryptionAlgorithm
*
* @param {object} params parameters
@@ -53,126 +52,170 @@ module.exports.DECRYPTION_CLASSES = {};
* @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
*/
const EncryptionAlgorithm = function(params) {
this._userId = params.userId;
this._deviceId = params.deviceId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
};
/** */
module.exports.EncryptionAlgorithm = EncryptionAlgorithm;
class EncryptionAlgorithm {
constructor(params) {
this._userId = params.userId;
this._deviceId = params.deviceId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
}
/**
* Encrypt a message event
*
* @method module:crypto/algorithms/base.EncryptionAlgorithm#encryptMessage
* @abstract
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
/**
* Encrypt a message event
*
* @method module:crypto/algorithms/base.EncryptionAlgorithm.encryptMessage
* @abstract
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
/**
* Called when the membership of a member of the room changes.
*
* @param {module:models/event.MatrixEvent} event event causing the change
* @param {module:models/room-member} member user whose membership changed
* @param {string=} oldMembership previous membership
*/
EncryptionAlgorithm.prototype.onRoomMembership = function(
event, member, oldMembership,
) {};
/**
* Called when the membership of a member of the room changes.
*
* @param {module:models/event.MatrixEvent} event event causing the change
* @param {module:models/room-member} member user whose membership changed
* @param {string=} oldMembership previous membership
* @public
*/
onRoomMembership(event, member, oldMembership) {
}
}
export {EncryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
/**
* base type for decryption implementations
*
* @constructor
* @alias module:crypto/algorithms/base.DecryptionAlgorithm
*
* @param {object} params parameters
* @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 {string=} params.roomId The ID of the room we will be receiving
* from. Null for to-device events.
*/
const DecryptionAlgorithm = function(params) {
this._userId = params.userId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._roomId = params.roomId;
};
/** */
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
class DecryptionAlgorithm {
constructor(params) {
this._userId = params.userId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
}
/**
* Decrypt an event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent
* @abstract
*
* @param {object} event raw event
*
* @return {null} if the event referred to an unknown megolm session
* @return {module:crypto.DecryptionResult} decryption result
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
/**
* Decrypt an event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent
* @abstract
*
* @param {MatrixEvent} event undecrypted event
*
* @return {Promise<module:crypto~EventDecryptionResult>} promise which
* resolves once we have finished decrypting. Rejects with an
* `algorithms.DecryptionError` if there is a problem decrypting the event.
*/
/**
* Handle a key event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
*
* @param {module:models/event.MatrixEvent} params event key event
*/
DecryptionAlgorithm.prototype.onRoomKeyEvent = function(params) {
// ignore by default
};
/**
* Handle a key event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
*
* @param {module:models/event.MatrixEvent} params event key event
*/
onRoomKeyEvent(params) {
// ignore by default
}
/**
* Import a room key
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
*/
DecryptionAlgorithm.prototype.importRoomKey = function(session) {
// ignore by default
};
/**
* Import a room key
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
*/
importRoomKey(session) {
// ignore by default
}
/**
* Determine if we have the keys necessary to respond to a room key request
*
* @param {module:crypto~IncomingRoomKeyRequest} keyRequest
* @return {Promise<boolean>} true if we have the keys and could (theoretically) share
* them; else false.
*/
hasKeysForKeyRequest(keyRequest) {
return Promise.resolve(false);
}
/**
* Send the response to a room key request
*
* @param {module:crypto~IncomingRoomKeyRequest} keyRequest
*/
shareKeysWithDevice(keyRequest) {
throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm");
}
}
export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
/**
* Exception thrown when decryption fails
*
* @constructor
* @param {string} msg message describing the problem
* @alias module:crypto/algorithms/base.DecryptionError
* @param {string} msg user-visible message describing the problem
*
* @param {Object=} details key/value pairs reported in the logs but not shown
* to the user.
*
* @extends Error
*/
module.exports.DecryptionError = function(msg) {
this.message = msg;
};
utils.inherits(module.exports.DecryptionError, Error);
class DecryptionError extends Error {
constructor(code, msg, details) {
super(msg);
this.code = code;
this.name = 'DecryptionError';
this.detailedString = _detailedStringForDecryptionError(this, details);
}
}
export {DecryptionError}; // https://github.com/jsdoc3/jsdoc/issues/1272
function _detailedStringForDecryptionError(err, details) {
let result = err.name + '[msg: ' + err.message;
if (details) {
result += ', ' +
Object.keys(details).map(
(k) => k + ': ' + details[k],
).join(', ');
}
result += ']';
return result;
}
/**
* Exception thrown specifically when we want to warn the user to consider
* the security of their conversation before continuing
*
* @constructor
* @param {string} msg message describing the problem
* @param {Object} devices userId -> {deviceId -> object}
* set of unknown devices per user we're warning about
* @extends Error
*/
module.exports.UnknownDeviceError = function(msg, devices) {
this.name = "UnknownDeviceError";
this.message = msg;
this.devices = devices;
};
utils.inherits(module.exports.UnknownDeviceError, Error);
export class UnknownDeviceError extends Error {
constructor(msg, devices) {
super(msg);
this.name = "UnknownDeviceError";
this.devices = devices;
}
}
/**
* Registers an encryption/decryption class for a particular algorithm
@@ -187,7 +230,7 @@ utils.inherits(module.exports.UnknownDeviceError, Error);
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
* implementation
*/
module.exports.registerAlgorithm = function(algorithm, encryptor, decryptor) {
module.exports.ENCRYPTION_CLASSES[algorithm] = encryptor;
module.exports.DECRYPTION_CLASSES[algorithm] = decryptor;
};
export function registerAlgorithm(algorithm, encryptor, decryptor) {
ENCRYPTION_CLASSES[algorithm] = encryptor;
DECRYPTION_CLASSES[algorithm] = decryptor;
}
+494 -164
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 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.
@@ -21,7 +22,7 @@ limitations under the License.
* @module crypto/algorithms/megolm
*/
const q = require("q");
import Promise from 'bluebird';
const utils = require("../../utils");
const olmlib = require("../olmlib");
@@ -74,6 +75,14 @@ OutboundSessionInfo.prototype.needsRotation = function(
return false;
};
OutboundSessionInfo.prototype.markSharedWithDevice = function(
userId, deviceId, chainIndex,
) {
if (!this.sharedWithDevices[userId]) {
this.sharedWithDevices[userId] = {};
}
this.sharedWithDevices[userId][deviceId] = chainIndex;
};
/**
* Determine if this session has been shared with devices which it shouldn't
@@ -132,7 +141,7 @@ function MegolmEncryption(params) {
// are using, and which devices we have shared the keys with. It resolves
// with an OutboundSessionInfo (or undefined, for the first message in the
// room).
this._setupPromise = q();
this._setupPromise = Promise.resolve();
// default rotation periods
this._sessionRotationPeriodMsgs = 100;
@@ -166,7 +175,7 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
// Updates `session` to hold the final OutboundSessionInfo.
//
// returns a promise which resolves once the keyshare is successful.
function prepareSession(oldSession) {
async function prepareSession(oldSession) {
session = oldSession;
// need to make a brand new session?
@@ -183,7 +192,8 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
}
if (!session) {
session = self._prepareNewSession();
console.log(`Starting new megolm session for room ${self._roomId}`);
session = await self._prepareNewSession();
}
// now check if we need to share with any devices
@@ -244,16 +254,157 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
*
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*/
MegolmEncryption.prototype._prepareNewSession = function() {
const session_id = this._olmDevice.createOutboundGroupSession();
const key = this._olmDevice.getOutboundGroupSessionKey(session_id);
MegolmEncryption.prototype._prepareNewSession = async function() {
const sessionId = this._olmDevice.createOutboundGroupSession();
const key = this._olmDevice.getOutboundGroupSessionKey(sessionId);
this._olmDevice.addInboundGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, session_id,
await this._olmDevice.addInboundGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId,
key.key, {ed25519: this._olmDevice.deviceEd25519Key},
);
return new OutboundSessionInfo(session_id);
return new OutboundSessionInfo(sessionId);
};
/**
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {number} chainIndex current chain index
*
* @param {object<userId, deviceId>} devicemap
* mapping from userId to deviceId to {@link module:crypto~OlmSessionResult}
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @return {array<object<userid, deviceInfo>>}
*/
MegolmEncryption.prototype._splitUserDeviceMap = function(
session, chainIndex, devicemap, devicesByUser,
) {
const maxToDeviceMessagesPerRequest = 20;
// use an array where the slices of a content map gets stored
const mapSlices = [];
let currentSliceId = 0; // start inserting in the first slice
let entriesInCurrentSlice = 0;
for (const userId of Object.keys(devicesByUser)) {
const devicesToShareWith = devicesByUser[userId];
const sessionResults = devicemap[userId];
for (let i = 0; i < devicesToShareWith.length; i++) {
const deviceInfo = devicesToShareWith[i];
const deviceId = deviceInfo.deviceId;
const sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
//
// we could send them a to_device message anyway, as a
// signal that they have missed out on the key sharing
// message because of the lack of keys, but there's not
// much point in that really; it will mostly serve to clog
// up to_device inboxes.
// mark this device as "handled" because we don't want to try
// to claim a one-time-key for dead devices on every message.
session.markSharedWithDevice(userId, deviceId, chainIndex);
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
}
console.log(
"share keys with device " + userId + ":" + deviceId,
);
if (entriesInCurrentSlice > maxToDeviceMessagesPerRequest) {
// the current slice is filled up. Start inserting into the next slice
entriesInCurrentSlice = 0;
currentSliceId++;
}
if (!mapSlices[currentSliceId]) {
mapSlices[currentSliceId] = [];
}
mapSlices[currentSliceId].push({
userId: userId,
deviceInfo: deviceInfo,
});
entriesInCurrentSlice++;
}
}
return mapSlices;
};
/**
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {number} chainIndex current chain index
*
* @param {object<userId, deviceInfo>} userDeviceMap
* mapping from userId to deviceInfo
*
* @param {object} payload fields to include in the encrypted payload
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* for the given userDeviceMap is generated and has been sent.
*/
MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
session, chainIndex, userDeviceMap, payload,
) {
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
const contentMap = {};
const promises = [];
for (let i = 0; i < userDeviceMap.length; i++) {
const val = userDeviceMap[i];
const userId = val.userId;
const deviceInfo = val.deviceInfo;
const deviceId = deviceInfo.deviceId;
if (!contentMap[userId]) {
contentMap[userId] = {};
}
contentMap[userId][deviceId] = encryptedContent;
promises.push(
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._userId,
this._deviceId,
this._olmDevice,
userId,
deviceInfo,
payload,
),
);
}
return Promise.all(promises).then(() => {
return this._baseApis.sendToDevice("m.room.encrypted", contentMap).then(() => {
// store that we successfully uploaded the keys of the current slice
for (const userId of Object.keys(contentMap)) {
for (const deviceId of Object.keys(contentMap[userId])) {
session.markSharedWithDevice(
userId, deviceId, chainIndex,
);
}
}
});
});
};
/**
@@ -263,13 +414,8 @@ MegolmEncryption.prototype._prepareNewSession = function() {
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* message has been sent.
*/
MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUser) {
const self = this;
MegolmEncryption.prototype._shareKeyWithDevices = async function(session, devicesByUser) {
const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
const payload = {
type: "m.room_key",
@@ -282,98 +428,28 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUse
},
};
const contentMap = {};
return olmlib.ensureOlmSessionsForDevices(
const devicemap = await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser,
).then(function(devicemap) {
let haveTargets = false;
);
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
const userDeviceMaps = this._splitUserDeviceMap(
session, key.chain_index, devicemap, devicesByUser,
);
const devicesToShareWith = devicesByUser[userId];
const sessionResults = devicemap[userId];
for (let i = 0; i < userDeviceMaps.length; i++) {
try {
await this._encryptAndSendKeysToDevices(
session, key.chain_index, userDeviceMaps[i], payload,
);
console.log(`Completed megolm keyshare in ${this._roomId} `
+ `(slice ${i + 1}/${userDeviceMaps.length})`);
} catch (e) {
console.log(`megolm keyshare in ${this._roomId} `
+ `(slice ${i + 1}/${userDeviceMaps.length}) failed`);
for (let i = 0; i < devicesToShareWith.length; i++) {
const deviceInfo = devicesToShareWith[i];
const deviceId = deviceInfo.deviceId;
const sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
//
// we could send them a to_device message anyway, as a
// signal that they have missed out on the key sharing
// message because of the lack of keys, but there's not
// much point in that really; it will mostly serve to clog
// up to_device inboxes.
//
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
}
console.log(
"sharing keys with device " + userId + ":" + deviceId,
);
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId,
self._deviceId,
self._olmDevice,
userId,
deviceInfo,
payload,
);
if (!contentMap[userId]) {
contentMap[userId] = {};
}
contentMap[userId][deviceId] = encryptedContent;
haveTargets = true;
}
throw e;
}
if (!haveTargets) {
return q();
}
// TODO: retries
return self._baseApis.sendToDevice("m.room.encrypted", contentMap);
}).then(function() {
// Add the devices we have shared with to session.sharedWithDevices.
//
// we deliberately iterate over devicesByUser (ie, the devices we
// attempted to share with) rather than the contentMap (those we did
// share with), because we don't want to try to claim a one-time-key
// for dead devices on every message.
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
if (!session.sharedWithDevices[userId]) {
session.sharedWithDevices[userId] = {};
}
const devicesToShareWith = devicesByUser[userId];
for (let i = 0; i < devicesToShareWith.length; i++) {
const deviceInfo = devicesToShareWith[i];
session.sharedWithDevices[userId][deviceInfo.deviceId] =
key.chain_index;
}
}
});
}
};
/**
@@ -387,6 +463,8 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUse
*/
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
const self = this;
console.log(`Starting to encrypt event for ${this._roomId}`);
return this._getDevicesInRoom(room).then(function(devicesInRoom) {
// check if any of these devices are not yet known to the user.
// if so, warn the user so they can verify or ignore.
@@ -411,6 +489,8 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
// XXX: Do we still need this now that m.new_device messages
// no longer exist since #483?
device_id: self._deviceId,
};
@@ -419,6 +499,16 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
});
};
/**
* Forces the current outbound group session to be discarded such
* that another one will be created next time an event is sent.
*
* This should not normally be necessary.
*/
MegolmEncryption.prototype.forceDiscardSession = function() {
this._setupPromise = this._setupPromise.then(() => null);
};
/**
* Checks the devices we're about to send to and see if any are entirely
* unknown to the user. If so, warn the user, and mark them as known to
@@ -458,46 +548,46 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
* @return {module:client.Promise} Promise which resolves to a map
* from userId to deviceId to deviceInfo
*/
MegolmEncryption.prototype._getDevicesInRoom = function(room) {
// XXX what about rooms where invitees can see the content?
const roomMembers = utils.map(room.getJoinedMembers(), function(u) {
MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
const members = await room.getEncryptionTargetMembers();
const roomMembers = utils.map(members, function(u) {
return u.userId;
});
// The global value is treated as a default for when rooms don't specify a value.
let isBlacklisting = this._crypto.getGlobalBlacklistUnverifiedDevices();
if (typeof room.getBlacklistUnverifiedDevices() === 'boolean') {
isBlacklisting = room.getBlacklistUnverifiedDevices();
}
// We are happy to use a cached version here: we assume that if we already
// have a list of the user's devices, then we already share an e2e room
// with them, which means that they will have announced any new devices via
// an m.new_device.
//
// XXX: what if the cache is stale, and the user left the room we had in
// common and then added new devices before joining this one? --Matthew
//
// yup, see https://github.com/vector-im/riot-web/issues/2305 --richvdh
return this._crypto.downloadKeys(roomMembers, false).then((devices) => {
// remove any blocked devices
for (const userId in devices) {
if (!devices.hasOwnProperty(userId)) {
// device_lists in their /sync response. This cache should then be maintained
// using all the device_lists changes and left fields.
// See https://github.com/vector-im/riot-web/issues/2305 for details.
const devices = await this._crypto.downloadKeys(roomMembers, false);
// remove any blocked devices
for (const userId in devices) {
if (!devices.hasOwnProperty(userId)) {
continue;
}
const userDevices = devices[userId];
for (const deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
const userDevices = devices[userId];
for (const deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
if (userDevices[deviceId].isBlocked() ||
(userDevices[deviceId].isUnverified() &&
(room.getBlacklistUnverifiedDevices() ||
this._crypto.getGlobalBlacklistUnverifiedDevices()))
) {
delete userDevices[deviceId];
}
if (userDevices[deviceId].isBlocked() ||
(userDevices[deviceId].isUnverified() && isBlacklisting)
) {
delete userDevices[deviceId];
}
}
}
return devices;
});
return devices;
};
/**
@@ -513,8 +603,11 @@ function MegolmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
// events which we couldn't decrypt due to unknown sessions / indexes: map from
// senderKey|sessionId to list of MatrixEvents
// senderKey|sessionId to Set of MatrixEvents
this._pendingEvents = {};
// this gets stubbed out by the unit tests.
this.olmlib = olmlib;
}
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
@@ -523,38 +616,75 @@ utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
*
* @param {MatrixEvent} event
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
* returns a promise which resolves to a
* {@link module:crypto~EventDecryptionResult} once we have finished
* decrypting, or rejects with an `algorithms.DecryptionError` if there is a
* problem decrypting the event.
*/
MegolmDecryption.prototype.decryptEvent = function(event) {
MegolmDecryption.prototype.decryptEvent = async function(event) {
const content = event.getWireContent();
if (!content.sender_key || !content.session_id ||
!content.ciphertext
) {
throw new base.DecryptionError("Missing fields in input");
throw new base.DecryptionError(
"MEGOLM_MISSING_FIELDS",
"Missing fields in input",
);
}
// we add the event to the pending list *before* we start decryption.
//
// then, if the key turns up while decryption is in progress (and
// decryption fails), we will schedule a retry.
// (fixes https://github.com/vector-im/riot-web/issues/5001)
this._addEventToPendingList(event);
let res;
try {
res = this._olmDevice.decryptGroupMessage(
res = await this._olmDevice.decryptGroupMessage(
event.getRoomId(), content.sender_key, content.session_id, content.ciphertext,
event.getId(), event.getTs(),
);
} catch (e) {
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
this._addEventToPendingList(event);
this._requestKeysForEvent(event);
errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX';
}
throw new base.DecryptionError(e);
throw new base.DecryptionError(
errorCode,
e.toString(), {
session: content.sender_key + '|' + content.session_id,
},
);
}
if (res === null) {
// We've got a message for a session we don't have.
this._addEventToPendingList(event);
//
// (XXX: We might actually have received this key since we started
// decrypting, in which case we'll have scheduled a retry, and this
// request will be redundant. We could probably check to see if the
// event is still in the pending list; if not, a retry will have been
// scheduled, so we needn't send out the request here.)
this._requestKeysForEvent(event);
throw new base.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
"The sender's device has not sent us the keys for this message.",
{
session: content.sender_key + '|' + content.session_id,
},
);
}
// success. We can remove the event from the pending list, if that hasn't
// already happened.
this._removeEventFromPendingList(event);
const payload = JSON.parse(res.result);
// belt-and-braces check that the room id matches that indicated by the HS
@@ -562,17 +692,44 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
// room, so neither the sender nor a MITM can lie about the room_id).
if (payload.room_id !== event.getRoomId()) {
throw new base.DecryptionError(
"MEGOLM_BAD_ROOM",
"Message intended for room " + payload.room_id,
);
}
event.setClearData(payload, res.keysProved, res.keysClaimed);
return {
clearEvent: payload,
senderCurve25519Key: res.senderKey,
claimedEd25519Key: res.keysClaimed.ed25519,
forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain,
};
};
MegolmDecryption.prototype._requestKeysForEvent = function(event) {
const sender = event.getSender();
const wireContent = event.getWireContent();
// send the request to all of our own devices, and the
// original sending device if it wasn't us.
const recipients = [{
userId: this._userId, deviceId: '*',
}];
if (sender != this._userId) {
recipients.push({
userId: sender, deviceId: wireContent.device_id,
});
}
this._crypto.requestRoomKey({
room_id: event.getRoomId(),
algorithm: wireContent.algorithm,
sender_key: wireContent.sender_key,
session_id: wireContent.session_id,
}, recipients);
};
/**
* Add an event to the list of those we couldn't decrypt the first time we
* saw them.
* Add an event to the list of those awaiting their session keys.
*
* @private
*
@@ -582,37 +739,207 @@ MegolmDecryption.prototype._addEventToPendingList = function(event) {
const content = event.getWireContent();
const k = content.sender_key + "|" + content.session_id;
if (!this._pendingEvents[k]) {
this._pendingEvents[k] = [];
this._pendingEvents[k] = new Set();
}
this._pendingEvents[k].push(event);
this._pendingEvents[k].add(event);
};
/**
* Remove an event from the list of those awaiting their session keys.
*
* @private
*
* @param {module:models/event.MatrixEvent} event
*/
MegolmDecryption.prototype._removeEventFromPendingList = function(event) {
const content = event.getWireContent();
const k = content.sender_key + "|" + content.session_id;
if (!this._pendingEvents[k]) {
return;
}
this._pendingEvents[k].delete(event);
if (this._pendingEvents[k].size === 0) {
delete this._pendingEvents[k];
}
};
/**
* @inheritdoc
*
* @param {module:models/event.MatrixEvent} event key event
*/
MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
console.log("Adding key from ", event);
const content = event.getContent();
const sessionId = content.session_id;
let senderKey = event.getSenderKey();
let forwardingKeyChain = [];
let exportFormat = false;
let keysClaimed;
if (!content.room_id ||
!content.session_id ||
!sessionId ||
!content.session_key
) {
console.error("key event is missing fields");
return;
}
this._olmDevice.addInboundGroupSession(
content.room_id, event.getSenderKey(), content.session_id,
content.session_key, event.getKeysClaimed(),
);
if (!senderKey) {
console.error("key event has no sender key (not encrypted?)");
return;
}
// have another go at decrypting events sent with this session.
this._retryDecryption(event.getSenderKey, content.session_id);
if (event.getType() == "m.forwarded_room_key") {
exportFormat = true;
forwardingKeyChain = content.forwarding_curve25519_key_chain;
if (!utils.isArray(forwardingKeyChain)) {
forwardingKeyChain = [];
}
// copy content before we modify it
forwardingKeyChain = forwardingKeyChain.slice();
forwardingKeyChain.push(senderKey);
senderKey = content.sender_key;
if (!senderKey) {
console.error("forwarded_room_key event is missing sender_key field");
return;
}
const ed25519Key = content.sender_claimed_ed25519_key;
if (!ed25519Key) {
console.error(
`forwarded_room_key_event is missing sender_claimed_ed25519_key field`,
);
return;
}
keysClaimed = {
ed25519: ed25519Key,
};
} else {
keysClaimed = event.getKeysClaimed();
}
console.log(`Adding key for megolm session ${senderKey}|${sessionId}`);
this._olmDevice.addInboundGroupSession(
content.room_id, senderKey, forwardingKeyChain, sessionId,
content.session_key, keysClaimed,
exportFormat,
).then(() => {
// cancel any outstanding room key requests for this session
this._crypto.cancelRoomKeyRequest({
algorithm: content.algorithm,
room_id: content.room_id,
session_id: content.session_id,
sender_key: senderKey,
});
// have another go at decrypting events sent with this session.
this._retryDecryption(senderKey, sessionId);
}).catch((e) => {
console.error(`Error handling m.room_key_event: ${e}`);
});
};
/**
* @inheritdoc
*/
MegolmDecryption.prototype.hasKeysForKeyRequest = function(keyRequest) {
const body = keyRequest.requestBody;
return this._olmDevice.hasInboundSessionKeys(
body.room_id,
body.sender_key,
body.session_id,
// TODO: ratchet index
);
};
/**
* @inheritdoc
*/
MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) {
const userId = keyRequest.userId;
const deviceId = keyRequest.deviceId;
const deviceInfo = this._crypto.getStoredDevice(userId, deviceId);
const body = keyRequest.requestBody;
this.olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {
[userId]: [deviceInfo],
},
).then((devicemap) => {
const olmSessionResult = devicemap[userId][deviceId];
if (!olmSessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
//
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
return null;
}
console.log(
"sharing keys for session " + body.sender_key + "|"
+ body.session_id + " with device "
+ userId + ":" + deviceId,
);
return this._buildKeyForwardingMessage(
body.room_id, body.sender_key, body.session_id,
);
}).then((payload) => {
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
return this.olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._userId,
this._deviceId,
this._olmDevice,
userId,
deviceInfo,
payload,
).then(() => {
const contentMap = {
[userId]: {
[deviceId]: encryptedContent,
},
};
// TODO: retries
return this._baseApis.sendToDevice("m.room.encrypted", contentMap);
});
}).done();
};
MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
roomId, senderKey, sessionId,
) {
const key = await this._olmDevice.getInboundGroupSessionKey(
roomId, senderKey, sessionId,
);
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,
},
};
};
/**
* @inheritdoc
@@ -620,10 +947,18 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
* @param {module:crypto/OlmDevice.MegolmSessionData} session
*/
MegolmDecryption.prototype.importRoomKey = function(session) {
this._olmDevice.importInboundGroupSession(session);
// have another go at decrypting events sent with this session.
this._retryDecryption(session.sender_key, session.session_id);
return this._olmDevice.addInboundGroupSession(
session.room_id,
session.sender_key,
session.forwarding_curve25519_key_chain,
session.session_id,
session.session_key,
session.sender_claimed_keys,
true,
).then(() => {
// have another go at decrypting events sent with this session.
this._retryDecryption(session.sender_key, session.session_id);
});
};
/**
@@ -642,13 +977,8 @@ MegolmDecryption.prototype._retryDecryption = function(senderKey, sessionId) {
delete this._pendingEvents[k];
for (let i = 0; i < pending.length; i++) {
try {
this.decryptEvent(pending[i]);
console.log("successful re-decryption of", pending[i]);
} catch (e) {
console.log("Still can't decrypt", pending[i], e.stack || e);
}
for (const ev of pending) {
ev.attemptDecryption(this._crypto);
}
};
+92 -72
View File
@@ -20,7 +20,7 @@ limitations under the License.
*
* @module crypto/algorithms/olm
*/
const q = require('q');
import Promise from 'bluebird';
const utils = require("../../utils");
const olmlib = require("../olmlib");
@@ -60,11 +60,11 @@ OlmEncryption.prototype._ensureSession = function(roomMembers) {
if (this._sessionPrepared) {
// prep already done
return q();
return Promise.resolve();
}
const self = this;
this._prepPromise = self._crypto.downloadKeys(roomMembers, true).then(function(res) {
this._prepPromise = self._crypto.downloadKeys(roomMembers).then(function(res) {
return self._crypto.ensureOlmSessionsForUsers(roomMembers);
}).then(function() {
self._sessionPrepared = true;
@@ -83,56 +83,62 @@ OlmEncryption.prototype._ensureSession = function(roomMembers) {
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
OlmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
// pick the list of recipients based on the membership list.
//
// TODO: there is a race condition here! What if a new user turns up
// just as you are sending a secret message?
const users = utils.map(room.getJoinedMembers(), function(u) {
const members = await room.getEncryptionTargetMembers();
const users = utils.map(members, function(u) {
return u.userId;
});
const self = this;
return this._ensureSession(users).then(function() {
const payloadFields = {
room_id: room.roomId,
type: eventType,
content: content,
};
await this._ensureSession(users);
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
const payloadFields = {
room_id: room.roomId,
type: eventType,
content: content,
};
for (let i = 0; i < users.length; ++i) {
const userId = users[i];
const devices = self._crypto.getStoredDevicesForUser(userId);
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
for (let j = 0; j < devices.length; ++j) {
const deviceInfo = devices[j];
const key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
// don't bother setting up sessions with blocked users
continue;
}
const promises = [];
for (let i = 0; i < users.length; ++i) {
const userId = users[i];
const devices = self._crypto.getStoredDevicesForUser(userId);
for (let j = 0; j < devices.length; ++j) {
const deviceInfo = devices[j];
const key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
// don't bother setting up sessions with blocked users
continue;
}
promises.push(
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId, self._deviceId, self._olmDevice,
userId, deviceInfo, payloadFields,
);
}
),
);
}
}
return encryptedContent;
});
return await Promise.all(promises).return(encryptedContent);
};
/**
@@ -153,33 +159,42 @@ utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
*
* @param {MatrixEvent} event
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
* returns a promise which resolves to a
* {@link module:crypto~EventDecryptionResult} once we have finished
* decrypting. Rejects with an `algorithms.DecryptionError` if there is a
* problem decrypting the event.
*/
OlmDecryption.prototype.decryptEvent = function(event) {
OlmDecryption.prototype.decryptEvent = async function(event) {
const content = event.getWireContent();
const deviceKey = content.sender_key;
const ciphertext = content.ciphertext;
if (!ciphertext) {
throw new base.DecryptionError("Missing ciphertext");
throw new base.DecryptionError(
"OLM_MISSING_CIPHERTEXT",
"Missing ciphertext",
);
}
if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) {
throw new base.DecryptionError("Not included in recipients");
throw new base.DecryptionError(
"OLM_NOT_INCLUDED_IN_RECIPIENTS",
"Not included in recipients",
);
}
const message = ciphertext[this._olmDevice.deviceCurve25519Key];
let payloadString;
try {
payloadString = this._decryptMessage(deviceKey, message);
payloadString = await this._decryptMessage(deviceKey, message);
} catch (e) {
console.warn(
"Failed to decrypt Olm event (id=" +
event.getId() + ") from " + deviceKey +
": " + e.message,
throw new base.DecryptionError(
"OLM_BAD_ENCRYPTED_MESSAGE",
"Bad Encrypted Message", {
sender: deviceKey,
err: e,
},
);
throw new base.DecryptionError("Bad Encrypted Message");
}
const payload = JSON.parse(payloadString);
@@ -187,22 +202,20 @@ OlmDecryption.prototype.decryptEvent = function(event) {
// check that we were the intended recipient, to avoid unknown-key attack
// https://github.com/vector-im/vector-web/issues/2483
if (payload.recipient != this._userId) {
console.warn(
"Event " + event.getId() + ": Intended recipient " +
payload.recipient + " does not match our id " + this._userId,
);
throw new base.DecryptionError(
"OLM_BAD_RECIPIENT",
"Message was intented for " + payload.recipient,
);
}
if (payload.recipient_keys.ed25519 !=
this._olmDevice.deviceEd25519Key) {
console.warn(
"Event " + event.getId() + ": Intended recipient ed25519 key " +
payload.recipient_keys.ed25519 + " did not match ours",
if (payload.recipient_keys.ed25519 != this._olmDevice.deviceEd25519Key) {
throw new base.DecryptionError(
"OLM_BAD_RECIPIENT_KEY",
"Message not intended for this device", {
intended: payload.recipient_keys.ed25519,
our_key: this._olmDevice.deviceEd25519Key,
},
);
throw new base.DecryptionError("Message not intended for this device");
}
// check that the original sender matches what the homeserver told us, to
@@ -210,29 +223,32 @@ OlmDecryption.prototype.decryptEvent = function(event) {
// (this check is also provided via the sender's embedded ed25519 key,
// which is checked elsewhere).
if (payload.sender != event.getSender()) {
console.warn(
"Event " + event.getId() + ": original sender " + payload.sender +
" does not match reported sender " + event.getSender(),
);
throw new base.DecryptionError(
"Message forwarded from " + payload.sender,
"OLM_FORWARDED_MESSAGE",
"Message forwarded from " + payload.sender, {
reported_sender: event.getSender(),
},
);
}
// Olm events intended for a room have a room_id.
if (payload.room_id !== event.getRoomId()) {
console.warn(
"Event " + event.getId() + ": original room " + payload.room_id +
" does not match reported room " + event.room_id,
);
throw new base.DecryptionError(
"Message intended for room " + payload.room_id,
"OLM_BAD_ROOM",
"Message intended for room " + payload.room_id, {
reported_room: event.room_id,
},
);
}
event.setClearData(payload, {curve25519: deviceKey}, payload.keys || {});
};
const claimedKeys = payload.keys || {};
return {
clearEvent: payload,
senderCurve25519Key: deviceKey,
claimedEd25519Key: claimedKeys.ed25519 || null,
};
};
/**
* Attempt to decrypt an Olm message
@@ -242,15 +258,19 @@ OlmDecryption.prototype.decryptEvent = function(event) {
*
* @return {string} payload, if decrypted successfully.
*/
OlmDecryption.prototype._decryptMessage = function(theirDeviceIdentityKey, message) {
const sessionIds = this._olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
OlmDecryption.prototype._decryptMessage = async function(
theirDeviceIdentityKey, message,
) {
const sessionIds = await this._olmDevice.getSessionIdsForDevice(
theirDeviceIdentityKey,
);
// try each session in turn.
const decryptionErrors = {};
for (let i = 0; i < sessionIds.length; i++) {
const sessionId = sessionIds[i];
try {
const payload = this._olmDevice.decryptMessage(
const payload = await this._olmDevice.decryptMessage(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
console.log(
@@ -259,7 +279,7 @@ OlmDecryption.prototype._decryptMessage = function(theirDeviceIdentityKey, messa
);
return payload;
} catch (e) {
const foundSession = this._olmDevice.matchesSession(
const foundSession = await this._olmDevice.matchesSession(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
@@ -297,7 +317,7 @@ OlmDecryption.prototype._decryptMessage = function(theirDeviceIdentityKey, messa
let res;
try {
res = this._olmDevice.createInboundSession(
res = await this._olmDevice.createInboundSession(
theirDeviceIdentityKey, message.type, message.body,
);
} catch (e) {
+867 -408
View File
File diff suppressed because it is too large Load Diff
+58 -47
View File
@@ -20,7 +20,7 @@ limitations under the License.
* Utilities common to olm encryption algorithms
*/
const q = require('q');
import Promise from 'bluebird';
const anotherjson = require('another-json');
const utils = require("../utils");
@@ -48,14 +48,17 @@ module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
* @param {string} recipientUserId
* @param {module:crypto/deviceinfo} recipientDevice
* @param {object} payloadFields fields to include in the encrypted payload
*
* Returns a promise which resolves (to undefined) when the payload
* has been encrypted into `resultsObject`
*/
module.exports.encryptMessageForDevice = function(
module.exports.encryptMessageForDevice = async function(
resultsObject,
ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice,
payloadFields,
) {
const deviceKey = recipientDevice.getIdentityKey();
const sessionId = olmDevice.getSessionIdForDevice(deviceKey);
const sessionId = await olmDevice.getSessionIdForDevice(deviceKey);
if (sessionId === null) {
// If we don't have a session for a device then
// we can't encrypt a message for it.
@@ -99,7 +102,7 @@ module.exports.encryptMessageForDevice = function(
utils.extend(payload, payloadFields);
resultsObject[deviceKey] = olmDevice.encryptMessage(
resultsObject[deviceKey] = await olmDevice.encryptMessage(
deviceKey, sessionId, JSON.stringify(payload),
);
};
@@ -118,7 +121,7 @@ module.exports.encryptMessageForDevice = function(
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
module.exports.ensureOlmSessionsForDevices = function(
module.exports.ensureOlmSessionsForDevices = async function(
olmDevice, baseApis, devicesByUser,
) {
const devicesWithoutSession = [
@@ -136,7 +139,7 @@ module.exports.ensureOlmSessionsForDevices = function(
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
const sessionId = olmDevice.getSessionIdForDevice(key);
const sessionId = await olmDevice.getSessionIdForDevice(key);
if (sessionId === null) {
devicesWithoutSession.push([userId, deviceId]);
}
@@ -148,7 +151,7 @@ module.exports.ensureOlmSessionsForDevices = function(
}
if (devicesWithoutSession.length === 0) {
return q(result);
return result;
}
// TODO: this has a race condition - if we try to send another message
@@ -158,55 +161,60 @@ module.exports.ensureOlmSessionsForDevices = function(
// That should eventually resolve itself, but it's poor form.
const oneTimeKeyAlgorithm = "signed_curve25519";
return baseApis.claimOneTimeKeys(
const res = await baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm,
).then(function(res) {
const otk_res = res.one_time_keys || {};
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
);
const otk_res = res.one_time_keys || {};
const promises = [];
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
const userRes = otk_res[userId] || {};
const devices = devicesByUser[userId];
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
if (result[userId][deviceId].sessionId) {
// we already have a result for this device
continue;
}
const userRes = otk_res[userId] || {};
const devices = devicesByUser[userId];
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
if (result[userId][deviceId].sessionId) {
// we already have a result for this device
continue;
}
const deviceRes = userRes[deviceId] || {};
let oneTimeKey = null;
for (const keyId in deviceRes) {
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
oneTimeKey = deviceRes[keyId];
}
const deviceRes = userRes[deviceId] || {};
let oneTimeKey = null;
for (const keyId in deviceRes) {
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
oneTimeKey = deviceRes[keyId];
}
if (!oneTimeKey) {
console.warn(
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId,
);
continue;
}
const sid = _verifyKeyAndStartSession(
olmDevice, oneTimeKey, userId, deviceInfo,
);
result[userId][deviceId].sessionId = sid;
}
if (!oneTimeKey) {
console.warn(
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId,
);
continue;
}
promises.push(
_verifyKeyAndStartSession(
olmDevice, oneTimeKey, userId, deviceInfo,
).then((sid) => {
result[userId][deviceId].sessionId = sid;
}),
);
}
return result;
});
}
await Promise.all(promises);
return result;
};
function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
const deviceId = deviceInfo.deviceId;
try {
_verifySignature(
await _verifySignature(
olmDevice, oneTimeKey, userId, deviceId,
deviceInfo.getFingerprint(),
);
@@ -220,7 +228,7 @@ function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
let sid;
try {
sid = olmDevice.createOutboundSession(
sid = await olmDevice.createOutboundSession(
deviceInfo.getIdentityKey(), oneTimeKey.key,
);
} catch (e) {
@@ -249,8 +257,11 @@ function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
* @param {string} signingDeviceId ID of the device whose signature should be checked
*
* @param {string} signingKey base64-ed ed25519 public key
*
* Returns a promise which resolves (to undefined) if the the signature is good,
* or rejects with an Error if it is bad.
*/
const _verifySignature = module.exports.verifySignature = function(
const _verifySignature = module.exports.verifySignature = async function(
olmDevice, obj, signingUserId, signingDeviceId, signingKey,
) {
const signKeyId = "ed25519:" + signingDeviceId;
+34
View File
@@ -0,0 +1,34 @@
/**
* Internal module. Defintions for storage for the crypto module
*
* @module
*/
/**
* Abstraction of things that can store data required for end-to-end encryption
*
* @interface CryptoStore
*/
/**
* Represents an outgoing room key request
*
* @typedef {Object} OutgoingRoomKeyRequest
*
* @property {string} requestId unique id for this request. Used for both
* an id within the request for later pairing with a cancellation, and for
* the transaction id when sending the to_device messages to our local
* server.
*
* @property {string?} cancellationTxnId
* transaction id for the cancellation, if any
*
* @property {Array<{userId: string, deviceId: string}>} recipients
* list of recipients for the request
*
* @property {module:crypto~RoomKeyRequestBody} requestBody
* parameters for the request.
*
* @property {Number} state current state of this request (states are defined
* in {@link module:crypto/OutgoingRoomKeyRequestManager~ROOM_KEY_REQUEST_STATES})
*/
@@ -0,0 +1,550 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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 Promise from 'bluebird';
import utils from '../../utils';
export const VERSION = 6;
/**
* Implementation of a CryptoStore which is backed by an existing
* IndexedDB connection. Generally you want IndexedDBCryptoStore
* which connects to the database and defers to one of these.
*
* @implements {module:crypto/store/base~CryptoStore}
*/
export class Backend {
/**
* @param {IDBDatabase} db
*/
constructor(db) {
this._db = db;
// make sure we close the db on `onversionchange` - otherwise
// attempts to delete the database will block (and subsequent
// attempts to re-create it will also block).
db.onversionchange = (ev) => {
console.log(`versionchange for indexeddb ${this._dbName}: closing`);
db.close();
};
}
/**
* Look for an existing outgoing room key request, and if none is found,
* add a new one
*
* @param {module:crypto/store/base~OutgoingRoomKeyRequest} request
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one.
*/
getOrAddOutgoingRoomKeyRequest(request) {
const requestBody = request.requestBody;
const deferred = Promise.defer();
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
txn.onerror = deferred.reject;
// first see if we already have an entry for this request.
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
if (existing) {
// this entry matches the request - return it.
console.log(
`already have key request outstanding for ` +
`${requestBody.room_id} / ${requestBody.session_id}: ` +
`not sending another`,
);
deferred.resolve(existing);
return;
}
// we got to the end of the list without finding a match
// - add the new request.
console.log(
`enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id,
);
txn.oncomplete = () => { deferred.resolve(request); };
const store = txn.objectStore("outgoingRoomKeyRequests");
store.add(request);
});
return deferred.promise;
}
/**
* Look for an existing room key request
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
*
* @return {Promise} resolves to the matching
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
const deferred = Promise.defer();
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
txn.onerror = deferred.reject;
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
deferred.resolve(existing);
});
return deferred.promise;
}
/**
* look for an existing room key request in the db
*
* @private
* @param {IDBTransaction} txn database transaction
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
* @param {Function} callback function to call with the results of the
* search. Either passed a matching
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found.
*/
_getOutgoingRoomKeyRequest(txn, requestBody, callback) {
const store = txn.objectStore("outgoingRoomKeyRequests");
const idx = store.index("session");
const cursorReq = idx.openCursor([
requestBody.room_id,
requestBody.session_id,
]);
cursorReq.onsuccess = (ev) => {
const cursor = ev.target.result;
if(!cursor) {
// no match found
callback(null);
return;
}
const existing = cursor.value;
if (utils.deepCompare(existing.requestBody, requestBody)) {
// got a match
callback(existing);
return;
}
// look at the next entry in the index
cursor.continue();
};
}
/**
* Look for room key requests by state
*
* @param {Array<Number>} wantedStates list of acceptable states
*
* @return {Promise} resolves to the a
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* there are no pending requests in those states. If there are multiple
* requests in those states, an arbitrary one is chosen.
*/
getOutgoingRoomKeyRequestByState(wantedStates) {
if (wantedStates.length === 0) {
return Promise.resolve(null);
}
// this is a bit tortuous because we need to make sure we do the lookup
// in a single transaction, to avoid having a race with the insertion
// code.
// index into the wantedStates array
let stateIndex = 0;
let result;
function onsuccess(ev) {
const cursor = ev.target.result;
if (cursor) {
// got a match
result = cursor.value;
return;
}
// try the next state in the list
stateIndex++;
if (stateIndex >= wantedStates.length) {
// no matches
return;
}
const wantedState = wantedStates[stateIndex];
const cursorReq = ev.target.source.openCursor(wantedState);
cursorReq.onsuccess = onsuccess;
}
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests");
const wantedState = wantedStates[stateIndex];
const cursorReq = store.index("state").openCursor(wantedState);
cursorReq.onsuccess = onsuccess;
return promiseifyTxn(txn).then(() => result);
}
/**
* Look for an existing room key request by id and state, and update it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
* @param {Object} updates name/value map of updates to apply
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found
*/
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
let result = null;
function onsuccess(ev) {
const cursor = ev.target.result;
if (!cursor) {
return;
}
const data = cursor.value;
if (data.state != expectedState) {
console.warn(
`Cannot update room key request from ${expectedState} ` +
`as it was already updated to ${data.state}`,
);
return;
}
Object.assign(data, updates);
cursor.update(data);
result = data;
}
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursorReq = txn.objectStore("outgoingRoomKeyRequests")
.openCursor(requestId);
cursorReq.onsuccess = onsuccess;
return promiseifyTxn(txn).then(() => result);
}
/**
* Look for an existing room key request by id and state, and delete it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
*
* @returns {Promise} resolves once the operation is completed
*/
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursorReq = txn.objectStore("outgoingRoomKeyRequests")
.openCursor(requestId);
cursorReq.onsuccess = (ev) => {
const cursor = ev.target.result;
if (!cursor) {
return;
}
const data = cursor.value;
if (data.state != expectedState) {
console.warn(
`Cannot delete room key request in state ${data.state} `
+ `(expected ${expectedState})`,
);
return;
}
cursor.delete();
};
return promiseifyTxn(txn);
}
// Olm Account
getAccount(txn, func) {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("-");
getReq.onsuccess = function() {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, e);
}
};
}
storeAccount(txn, newData) {
const objectStore = txn.objectStore("account");
objectStore.put(newData, "-");
}
// Olm Sessions
countEndToEndSessions(txn, func) {
const objectStore = txn.objectStore("sessions");
const countReq = objectStore.count();
countReq.onsuccess = function() {
func(countReq.result);
};
}
getEndToEndSessions(deviceKey, txn, func) {
const objectStore = txn.objectStore("sessions");
const idx = objectStore.index("deviceKey");
const getReq = idx.openCursor(deviceKey);
const results = {};
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
results[cursor.value.sessionId] = cursor.value.session;
cursor.continue();
} else {
try {
func(results);
} catch (e) {
abortWithException(txn, e);
}
}
};
}
getEndToEndSession(deviceKey, sessionId, txn, func) {
const objectStore = txn.objectStore("sessions");
const getReq = objectStore.get([deviceKey, sessionId]);
getReq.onsuccess = function() {
try {
if (getReq.result) {
func(getReq.result.session);
} else {
func(null);
}
} catch (e) {
abortWithException(txn, e);
}
};
}
storeEndToEndSession(deviceKey, sessionId, session, txn) {
const objectStore = txn.objectStore("sessions");
objectStore.put({deviceKey, sessionId, session});
}
// Inbound group sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.get([senderCurve25519Key, sessionId]);
getReq.onsuccess = function() {
try {
if (getReq.result) {
func(getReq.result.session);
} else {
func(null);
}
} catch (e) {
abortWithException(txn, e);
}
};
}
getAllEndToEndInboundGroupSessions(txn, func) {
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
try {
func({
senderKey: cursor.value.senderCurve25519Key,
sessionId: cursor.value.sessionId,
sessionData: cursor.value.session,
});
} catch (e) {
abortWithException(txn, e);
}
cursor.continue();
} else {
try {
func(null);
} catch (e) {
abortWithException(txn, e);
}
}
};
}
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
const objectStore = txn.objectStore("inbound_group_sessions");
const addReq = objectStore.add({
senderCurve25519Key, sessionId, session: sessionData,
});
addReq.onerror = (ev) => {
if (addReq.error.name === 'ConstraintError') {
// This stops the error from triggering the txn's onerror
ev.stopPropagation();
// ...and this stops it from aborting the transaction
ev.preventDefault();
console.log(
"Ignoring duplicate inbound group session: " +
senderCurve25519Key + " / " + sessionId,
);
} else {
abortWithException(txn, new Error(
"Failed to add inbound group session: " + addReq.error,
));
}
};
}
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
const objectStore = txn.objectStore("inbound_group_sessions");
objectStore.put({
senderCurve25519Key, sessionId, session: sessionData,
});
}
getEndToEndDeviceData(txn, func) {
const objectStore = txn.objectStore("device_data");
const getReq = objectStore.get("-");
getReq.onsuccess = function() {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, e);
}
};
}
storeEndToEndDeviceData(deviceData, txn) {
const objectStore = txn.objectStore("device_data");
objectStore.put(deviceData, "-");
}
storeEndToEndRoom(roomId, roomInfo, txn) {
const objectStore = txn.objectStore("rooms");
objectStore.put(roomInfo, roomId);
}
getEndToEndRooms(txn, func) {
const rooms = {};
const objectStore = txn.objectStore("rooms");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
rooms[cursor.key] = cursor.value;
cursor.continue();
} else {
try {
func(rooms);
} catch (e) {
abortWithException(txn, e);
}
}
};
}
doTxn(mode, stores, func) {
const txn = this._db.transaction(stores, mode);
const promise = promiseifyTxn(txn);
const result = func(txn);
return promise.then(() => {
return result;
});
}
}
export function upgradeDatabase(db, oldVersion) {
console.log(
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
+ ` to ${VERSION}`,
);
if (oldVersion < 1) { // The database did not previously exist.
createDatabase(db);
}
if (oldVersion < 2) {
db.createObjectStore("account");
}
if (oldVersion < 3) {
const sessionsStore = db.createObjectStore("sessions", {
keyPath: ["deviceKey", "sessionId"],
});
sessionsStore.createIndex("deviceKey", "deviceKey");
}
if (oldVersion < 4) {
db.createObjectStore("inbound_group_sessions", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
if (oldVersion < 5) {
db.createObjectStore("device_data");
}
if (oldVersion < 6) {
db.createObjectStore("rooms");
}
// Expand as needed.
}
function createDatabase(db) {
const outgoingRoomKeyRequestsStore =
db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" });
// we assume that the RoomKeyRequestBody will have room_id and session_id
// properties, to make the index efficient.
outgoingRoomKeyRequestsStore.createIndex("session",
["requestBody.room_id", "requestBody.session_id"],
);
outgoingRoomKeyRequestsStore.createIndex("state", "state");
}
/*
* Aborts a transaction with a given exception
* The transaction promise will be rejected with this exception.
*/
function abortWithException(txn, e) {
// We cheekily stick our exception onto the transaction object here
// We could alternatively make the thing we pass back to the app
// an object containing the transaction and exception.
txn._mx_abortexception = e;
try {
txn.abort();
} catch (e) {
// sometimes we won't be able to abort the transaction
// (ie. if it's aborted or completed)
}
}
function promiseifyTxn(txn) {
return new Promise((resolve, reject) => {
txn.oncomplete = () => {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
}
resolve();
};
txn.onerror = () => {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
}
reject();
};
txn.onabort = () => reject(txn._mx_abortexception);
});
}
+455
View File
@@ -0,0 +1,455 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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 Promise from 'bluebird';
import LocalStorageCryptoStore from './localStorage-crypto-store';
import MemoryCryptoStore from './memory-crypto-store';
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
/**
* Internal module. indexeddb storage for e2e.
*
* @module
*/
/**
* An implementation of CryptoStore, which is normally backed by an indexeddb,
* but with fallback to MemoryCryptoStore.
*
* @implements {module:crypto/store/base~CryptoStore}
*/
export default class IndexedDBCryptoStore {
/**
* Create a new IndexedDBCryptoStore
*
* @param {IDBFactory} indexedDB global indexedDB instance
* @param {string} dbName name of db to connect to
*/
constructor(indexedDB, dbName) {
this._indexedDB = indexedDB;
this._dbName = dbName;
this._backendPromise = null;
}
/**
* Ensure the database exists and is up-to-date, or fall back to
* a local storage or in-memory store.
*
* @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend,
* or a MemoryCryptoStore
*/
_connect() {
if (this._backendPromise) {
return this._backendPromise;
}
this._backendPromise = new Promise((resolve, reject) => {
if (!this._indexedDB) {
reject(new Error('no indexeddb support available'));
return;
}
console.log(`connecting to indexeddb ${this._dbName}`);
const req = this._indexedDB.open(
this._dbName, IndexedDBCryptoStoreBackend.VERSION,
);
req.onupgradeneeded = (ev) => {
const db = ev.target.result;
const oldVersion = ev.oldVersion;
IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion);
};
req.onblocked = () => {
console.log(
`can't yet open IndexedDBCryptoStore because it is open elsewhere`,
);
};
req.onerror = (ev) => {
reject(ev.target.error);
};
req.onsuccess = (r) => {
const db = r.target.result;
console.log(`connected to indexeddb ${this._dbName}`);
resolve(new IndexedDBCryptoStoreBackend.Backend(db));
};
}).then((backend) => {
// Edge has IndexedDB but doesn't support compund keys which we use fairly extensively.
// Try a dummy query which will fail if the browser doesn't support compund keys, so
// we can fall back to a different backend.
return backend.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS],
(txn) => {
backend.getEndToEndInboundGroupSession('', '', txn, () => {});
}).then(() => {
return backend;
},
);
}).catch((e) => {
console.warn(
`unable to connect to indexeddb ${this._dbName}` +
`: falling back to localStorage store: ${e}`,
);
return new LocalStorageCryptoStore(global.localStorage);
}).catch((e) => {
console.warn(
`unable to open localStorage: falling back to in-memory store: ${e}`,
);
return new MemoryCryptoStore();
});
return this._backendPromise;
}
/**
* Delete all data from this store.
*
* @returns {Promise} resolves when the store has been cleared.
*/
deleteAllData() {
return new Promise((resolve, reject) => {
if (!this._indexedDB) {
reject(new Error('no indexeddb support available'));
return;
}
console.log(`Removing indexeddb instance: ${this._dbName}`);
const req = this._indexedDB.deleteDatabase(this._dbName);
req.onblocked = () => {
console.log(
`can't yet delete IndexedDBCryptoStore because it is open elsewhere`,
);
};
req.onerror = (ev) => {
reject(ev.target.error);
};
req.onsuccess = () => {
console.log(`Removed indexeddb instance: ${this._dbName}`);
resolve();
};
}).catch((e) => {
// in firefox, with indexedDB disabled, this fails with a
// DOMError. We treat this as non-fatal, so that people can
// still use the app.
console.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
});
}
/**
* Look for an existing outgoing room key request, and if none is found,
* add a new one
*
* @param {module:crypto/store/base~OutgoingRoomKeyRequest} request
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one.
*/
getOrAddOutgoingRoomKeyRequest(request) {
return this._connect().then((backend) => {
return backend.getOrAddOutgoingRoomKeyRequest(request);
});
}
/**
* Look for an existing room key request
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
*
* @return {Promise} resolves to the matching
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequest(requestBody);
});
}
/**
* Look for room key requests by state
*
* @param {Array<Number>} wantedStates list of acceptable states
*
* @return {Promise} resolves to the a
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* there are no pending requests in those states. If there are multiple
* requests in those states, an arbitrary one is chosen.
*/
getOutgoingRoomKeyRequestByState(wantedStates) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequestByState(wantedStates);
});
}
/**
* Look for an existing room key request by id and state, and update it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
* @param {Object} updates name/value map of updates to apply
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found
*/
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
return this._connect().then((backend) => {
return backend.updateOutgoingRoomKeyRequest(
requestId, expectedState, updates,
);
});
}
/**
* Look for an existing room key request by id and state, and delete it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
*
* @returns {Promise} resolves once the operation is completed
*/
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
return this._connect().then((backend) => {
return backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
});
}
// Olm Account
/*
* Get the account pickle from the store.
* This requires an active transaction. See doTxn().
*
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the account pickle
*/
getAccount(txn, func) {
this._backendPromise.value().getAccount(txn, func);
}
/*
* Write the account pickle to the store.
* This requires an active transaction. See doTxn().
*
* @param {*} txn An active transaction. See doTxn().
* @param {string} newData The new account pickle to store.
*/
storeAccount(txn, newData) {
this._backendPromise.value().storeAccount(txn, newData);
}
// Olm sessions
/**
* Returns the number of end-to-end sessions in the store
* @param {*} txn An active transaction. See doTxn().
* @param {function(int)} func Called with the count of sessions
*/
countEndToEndSessions(txn, func) {
this._backendPromise.value().countEndToEndSessions(txn, func);
}
/**
* Retrieve a specific end-to-end session between the logged-in user
* and another device.
* @param {string} deviceKey The public key of the other device.
* @param {string} sessionId The ID of the session to retrieve
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called with A map from sessionId
* to Base64 end-to-end session.
*/
getEndToEndSession(deviceKey, sessionId, txn, func) {
this._backendPromise.value().getEndToEndSession(deviceKey, sessionId, txn, func);
}
/**
* Retrieve the end-to-end sessions between the logged-in user and another
* device.
* @param {string} deviceKey The public key of the other device.
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called with A map from sessionId
* to Base64 end-to-end session.
*/
getEndToEndSessions(deviceKey, txn, func) {
this._backendPromise.value().getEndToEndSessions(deviceKey, txn, func);
}
/**
* Store a session between the logged-in user and another device
* @param {string} deviceKey The public key of the other device.
* @param {string} sessionId The ID for this end-to-end session.
* @param {string} session Base64 encoded end-to-end session.
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndSession(deviceKey, sessionId, session, txn) {
this._backendPromise.value().storeEndToEndSession(
deviceKey, sessionId, session, txn,
);
}
// Inbound group saessions
/**
* Retrieve the end-to-end inbound group session for a given
* server key and session ID
* @param {string} senderCurve25519Key The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called with A map from sessionId
* to Base64 end-to-end session.
*/
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
this._backendPromise.value().getEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, txn, func,
);
}
/**
* Fetches all inbound group sessions in the store
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called once for each group session
* in the store with an object having keys {senderKey, sessionId,
* sessionData}, then once with null to indicate the end of the list.
*/
getAllEndToEndInboundGroupSessions(txn, func) {
this._backendPromise.value().getAllEndToEndInboundGroupSessions(txn, func);
}
/**
* Adds an end-to-end inbound group session to the store.
* If there already exists an inbound group session with the same
* senderCurve25519Key and sessionID, the session will not be added.
* @param {string} senderCurve25519Key The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {object} sessionData The session data structure
* @param {*} txn An active transaction. See doTxn().
*/
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backendPromise.value().addEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
/**
* Writes an end-to-end inbound group session to the store.
* If there already exists an inbound group session with the same
* senderCurve25519Key and sessionID, it will be overwritten.
* @param {string} senderCurve25519Key The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {object} sessionData The session data structure
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backendPromise.value().storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
// End-to-end device tracking
/**
* Store the state of all tracked devices
* This contains devices for each user, a tracking state for each user
* and a sync token matching the point in time the snapshot represents.
* These all need to be written out in full each time such that the snapshot
* is always consistent, so they are stored in one object.
*
* @param {Object} deviceData
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndDeviceData(deviceData, txn) {
this._backendPromise.value().storeEndToEndDeviceData(deviceData, txn);
}
/**
* Get the state of all tracked devices
*
* @param {*} txn An active transaction. See doTxn().
* @param {function(Object)} func Function called with the
* device data
*/
getEndToEndDeviceData(txn, func) {
this._backendPromise.value().getEndToEndDeviceData(txn, func);
}
// End to End Rooms
/**
* Store the end-to-end state for a room.
* @param {string} roomId The room's ID.
* @param {object} roomInfo The end-to-end info for the room.
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndRoom(roomId, roomInfo, txn) {
this._backendPromise.value().storeEndToEndRoom(roomId, roomInfo, txn);
}
/**
* Get an object of roomId->roomInfo for all e2e rooms in the store
* @param {*} txn An active transaction. See doTxn().
* @param {function(Object)} func Function called with the end to end encrypted rooms
*/
getEndToEndRooms(txn, func) {
this._backendPromise.value().getEndToEndRooms(txn, func);
}
/**
* Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may
* only be called within a callback of either this function or
* one of the store functions operating on the same transaction.
*
* @param {string} mode 'readwrite' if you need to call setter
* functions with this transaction. Otherwise, 'readonly'.
* @param {string[]} stores List IndexedDBCryptoStore.STORE_*
* options representing all types of object that will be
* accessed or written to with this transaction.
* @param {function(*)} func Function called with the
* transaction object: an opaque object that should be passed
* to store functions.
* @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
* functions throwing an exception will cause this promise to
* reject with that exception. On synchronous backends, the
* exception will propagate to the caller of the getFoo method.
*/
doTxn(mode, stores, func) {
return this._connect().then((backend) => {
return backend.doTxn(mode, stores, func);
});
}
}
IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
@@ -0,0 +1,210 @@
/*
Copyright 2017, 2018 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 Promise from 'bluebird';
import MemoryCryptoStore from './memory-crypto-store.js';
/**
* Internal module. Partial localStorage backed storage for e2e.
* This is not a full crypto store, just the in-memory store with
* some things backed by localStorage. It exists because indexedDB
* is broken in Firefox private mode or set to, "will not remember
* history".
*
* @module
*/
const E2E_PREFIX = "crypto.";
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey;
}
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
}
function keyEndToEndRoomsPrefix(roomId) {
return KEY_ROOMS_PREFIX + roomId;
}
/**
* @implements {module:crypto/store/base~CryptoStore}
*/
export default class LocalStorageCryptoStore extends MemoryCryptoStore {
constructor(webStore) {
super();
this.store = webStore;
}
// Olm Sessions
countEndToEndSessions(txn, func) {
let count = 0;
for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) ++count;
}
func(count);
}
_getEndToEndSessions(deviceKey, txn, func) {
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
}
getEndToEndSession(deviceKey, sessionId, txn, func) {
const sessions = this._getEndToEndSessions(deviceKey);
func(sessions[sessionId] || {});
}
getEndToEndSessions(deviceKey, txn, func) {
func(this._getEndToEndSessions(deviceKey) || {});
}
storeEndToEndSession(deviceKey, sessionId, session, txn) {
const sessions = this._getEndToEndSessions(deviceKey) || {};
sessions[sessionId] = session;
setJsonItem(
this.store, keyEndToEndSessions(deviceKey), sessions,
);
}
// Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
func(getJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
));
}
getAllEndToEndInboundGroupSessions(txn, func) {
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
// we can't use split, as the components we are trying to split out
// might themselves contain '/' characters. We rely on the
// senderKey being a (32-byte) curve25519 key, base64-encoded
// (hence 43 characters long).
func({
senderKey: key.substr(KEY_INBOUND_SESSION_PREFIX.length, 43),
sessionId: key.substr(KEY_INBOUND_SESSION_PREFIX.length + 44),
sessionData: getJsonItem(this.store, key),
});
}
}
func(null);
}
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
const existing = getJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
);
if (!existing) {
this.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
}
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
setJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
sessionData,
);
}
getEndToEndDeviceData(txn, func) {
func(getJsonItem(
this.store, KEY_DEVICE_DATA,
));
}
storeEndToEndDeviceData(deviceData, txn) {
setJsonItem(
this.store, KEY_DEVICE_DATA, deviceData,
);
}
storeEndToEndRoom(roomId, roomInfo, txn) {
setJsonItem(
this.store, keyEndToEndRoomsPrefix(roomId), roomInfo,
);
}
getEndToEndRooms(txn, func) {
const result = {};
const prefix = keyEndToEndRoomsPrefix('');
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
if (key.startsWith(prefix)) {
const roomId = key.substr(prefix.length);
result[roomId] = getJsonItem(this.store, key);
}
}
func(result);
}
/**
* Delete all data from this store.
*
* @returns {Promise} Promise which resolves when the store has been cleared.
*/
deleteAllData() {
this.store.removeItem(KEY_END_TO_END_ACCOUNT);
return Promise.resolve();
}
// Olm account
getAccount(txn, func) {
const account = getJsonItem(this.store, KEY_END_TO_END_ACCOUNT);
func(account);
}
storeAccount(txn, newData) {
setJsonItem(
this.store, KEY_END_TO_END_ACCOUNT, newData,
);
}
doTxn(mode, stores, func) {
return Promise.resolve(func(null));
}
}
function getJsonItem(store, key) {
try {
// if the key is absent, store.getItem() returns null, and
// JSON.parse(null) === null, so this returns null.
return JSON.parse(store.getItem(key));
} catch (e) {
console.log("Error: Failed to get key %s: %s", key, e.stack || e);
console.log(e.stack);
}
return null;
}
function setJsonItem(store, key, val) {
store.setItem(key, JSON.stringify(val));
}
+301
View File
@@ -0,0 +1,301 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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 Promise from 'bluebird';
import utils from '../../utils';
/**
* Internal module. in-memory storage for e2e.
*
* @module
*/
/**
* @implements {module:crypto/store/base~CryptoStore}
*/
export default class MemoryCryptoStore {
constructor() {
this._outgoingRoomKeyRequests = [];
this._account = null;
// Map of {devicekey -> {sessionId -> session pickle}}
this._sessions = {};
// Map of {senderCurve25519Key+'/'+sessionId -> session data object}
this._inboundGroupSessions = {};
// Opaque device data object
this._deviceData = null;
// roomId -> Opaque roomInfo object
this._rooms = {};
}
/**
* Delete all data from this store.
*
* @returns {Promise} Promise which resolves when the store has been cleared.
*/
deleteAllData() {
return Promise.resolve();
}
/**
* Look for an existing outgoing room key request, and if none is found,
* add a new one
*
* @param {module:crypto/store/base~OutgoingRoomKeyRequest} request
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one.
*/
getOrAddOutgoingRoomKeyRequest(request) {
const requestBody = request.requestBody;
return Promise.try(() => {
// first see if we already have an entry for this request.
const existing = this._getOutgoingRoomKeyRequest(requestBody);
if (existing) {
// this entry matches the request - return it.
console.log(
`already have key request outstanding for ` +
`${requestBody.room_id} / ${requestBody.session_id}: ` +
`not sending another`,
);
return existing;
}
// we got to the end of the list without finding a match
// - add the new request.
console.log(
`enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id,
);
this._outgoingRoomKeyRequests.push(request);
return request;
});
}
/**
* Look for an existing room key request
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
*
* @return {Promise} resolves to the matching
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
return Promise.resolve(this._getOutgoingRoomKeyRequest(requestBody));
}
/**
* Looks for existing room key request, and returns the result synchronously.
*
* @internal
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
*
* @return {module:crypto/store/base~OutgoingRoomKeyRequest?}
* the matching request, or null if not found
*/
_getOutgoingRoomKeyRequest(requestBody) {
for (const existing of this._outgoingRoomKeyRequests) {
if (utils.deepCompare(existing.requestBody, requestBody)) {
return existing;
}
}
return null;
}
/**
* Look for room key requests by state
*
* @param {Array<Number>} wantedStates list of acceptable states
*
* @return {Promise} resolves to the a
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* there are no pending requests in those states
*/
getOutgoingRoomKeyRequestByState(wantedStates) {
for (const req of this._outgoingRoomKeyRequests) {
for (const state of wantedStates) {
if (req.state === state) {
return Promise.resolve(req);
}
}
}
return Promise.resolve(null);
}
/**
* Look for an existing room key request by id and state, and update it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
* @param {Object} updates name/value map of updates to apply
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found
*/
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
for (const req of this._outgoingRoomKeyRequests) {
if (req.requestId !== requestId) {
continue;
}
if (req.state != expectedState) {
console.warn(
`Cannot update room key request from ${expectedState} ` +
`as it was already updated to ${req.state}`,
);
return Promise.resolve(null);
}
Object.assign(req, updates);
return Promise.resolve(req);
}
return Promise.resolve(null);
}
/**
* Look for an existing room key request by id and state, and delete it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
*
* @returns {Promise} resolves once the operation is completed
*/
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
for (let i = 0; i < this._outgoingRoomKeyRequests.length; i++) {
const req = this._outgoingRoomKeyRequests[i];
if (req.requestId !== requestId) {
continue;
}
if (req.state != expectedState) {
console.warn(
`Cannot delete room key request in state ${req.state} `
+ `(expected ${expectedState})`,
);
return Promise.resolve(null);
}
this._outgoingRoomKeyRequests.splice(i, 1);
return Promise.resolve(req);
}
return Promise.resolve(null);
}
// Olm Account
getAccount(txn, func) {
func(this._account);
}
storeAccount(txn, newData) {
this._account = newData;
}
// Olm Sessions
countEndToEndSessions(txn, func) {
return Object.keys(this._sessions).length;
}
getEndToEndSession(deviceKey, sessionId, txn, func) {
const deviceSessions = this._sessions[deviceKey] || {};
func(deviceSessions[sessionId] || null);
}
getEndToEndSessions(deviceKey, txn, func) {
func(this._sessions[deviceKey] || {});
}
storeEndToEndSession(deviceKey, sessionId, session, txn) {
let deviceSessions = this._sessions[deviceKey];
if (deviceSessions === undefined) {
deviceSessions = {};
this._sessions[deviceKey] = deviceSessions;
}
deviceSessions[sessionId] = session;
}
// Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
func(this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] || null);
}
getAllEndToEndInboundGroupSessions(txn, func) {
for (const key of Object.keys(this._inboundGroupSessions)) {
// we can't use split, as the components we are trying to split out
// might themselves contain '/' characters. We rely on the
// senderKey being a (32-byte) curve25519 key, base64-encoded
// (hence 43 characters long).
func({
senderKey: key.substr(0, 43),
sessionId: key.substr(44),
sessionData: this._inboundGroupSessions[key],
});
}
func(null);
}
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
const k = senderCurve25519Key+'/'+sessionId;
if (this._inboundGroupSessions[k] === undefined) {
this._inboundGroupSessions[k] = sessionData;
}
}
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData;
}
// Device Data
getEndToEndDeviceData(txn, func) {
func(this._deviceData);
}
storeEndToEndDeviceData(deviceData, txn) {
this._deviceData = deviceData;
}
// E2E rooms
storeEndToEndRoom(roomId, roomInfo, txn) {
this._rooms[roomId] = roomInfo;
}
getEndToEndRooms(txn, func) {
func(this._rooms);
}
doTxn(mode, stores, func) {
return Promise.resolve(func(null));
}
}
+25
View File
@@ -0,0 +1,25 @@
// can't just do InvalidStoreError extends Error
// because of http://babeljs.io/docs/usage/caveats/#classes
function InvalidStoreError(reason, value) {
const message = `Store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
const instance = Reflect.construct(Error, [message]);
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
instance.reason = reason;
instance.value = value;
return instance;
}
InvalidStoreError.TOGGLED_LAZY_LOADING = "TOGGLED_LAZY_LOADING";
InvalidStoreError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error,
enumerable: false,
writable: true,
configurable: true,
},
});
Reflect.setPrototypeOf(InvalidStoreError, Error);
module.exports.InvalidStoreError = InvalidStoreError;
+4 -3
View File
@@ -97,11 +97,12 @@ FilterComponent.prototype._checkFields =
};
const self = this;
Object.keys(literal_keys).forEach(function(name) {
for (let n=0; n < Object.keys(literal_keys).length; n++) {
const name = Object.keys(literal_keys)[n];
const match_func = literal_keys[name];
const not_name = "not_" + name;
const disallowed_values = self[not_name];
if (disallowed_values.map(match_func)) {
if (disallowed_values.filter(match_func).length > 0) {
return false;
}
@@ -111,7 +112,7 @@ FilterComponent.prototype._checkFields =
return false;
}
}
});
}
const contains_url_filter = this.filter_json.contains_url;
if (contains_url_filter !== undefined) {
+11
View File
@@ -51,6 +51,17 @@ function Filter(userId, filterId) {
this.definition = {};
}
Filter.LAZY_LOADING_MESSAGES_FILTER = {
lazy_load_members: true,
};
Filter.LAZY_LOADING_SYNC_FILTER = {
room: {
state: Filter.LAZY_LOADING_MESSAGES_FILTER,
},
};
/**
* Get the ID of this filter on your homeserver (if known)
* @return {?Number} The filter ID
+217 -51
View File
@@ -18,7 +18,9 @@ limitations under the License.
* This is an internal module. See {@link MatrixHttpApi} for the public class.
* @module http-api
*/
const q = require("q");
import Promise from 'bluebird';
const parseContentType = require('content-type').parse;
const utils = require("./utils");
// we use our own implementation of setTimeout, so that if we get suspended in
@@ -64,7 +66,7 @@ module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
* @param {string} opts.prefix Required. The matrix client prefix to use, e.g.
* '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants.
*
* @param {bool=} opts.onlyData True to return only the 'data' component of the
* @param {boolean} opts.onlyData True to return only the 'data' component of the
* response (e.g. the parsed HTTP body). If false, requests will return an
* object with the properties <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
*
@@ -74,12 +76,15 @@ module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
* requests.
* @param {Number=} opts.localTimeoutMs The default maximum amount of time to wait
* before timing out the request. If not specified, there is no timeout.
* @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
* Authorization header instead of query param to send the access token to the server.
*/
module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
opts.onlyData = opts.onlyData || false;
this.event_emitter = event_emitter;
this.opts = opts;
this.useAuthorizationHeader = Boolean(opts.useAuthorizationHeader);
this.uploads = [];
};
@@ -113,6 +118,10 @@ module.exports.MatrixHttpApi.prototype = {
* @param {string=} opts.name Name to give the file on the server. Defaults
* to <tt>file.name</tt>.
*
* @param {boolean=} opts.includeFilename if false will not send the filename,
* e.g for encrypted file uploads where filename leaks are undesirable.
* Defaults to true.
*
* @param {string=} opts.type Content-type for the upload. Defaults to
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
*
@@ -129,6 +138,10 @@ module.exports.MatrixHttpApi.prototype = {
* invoke on success/failure. See the promise return values for more
* information.
*
* @param {Function=} opts.progressHandler Optional. Called when a chunk of
* data has been uploaded, with an object containing the fields `loaded`
* (number of bytes transferred) and `total` (total size, if known).
*
* @return {module:client.Promise} Resolves to response object, as
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
@@ -143,6 +156,9 @@ module.exports.MatrixHttpApi.prototype = {
opts = {};
}
// default opts.includeFilename to true (ignoring falsey values)
const includeFilename = opts.includeFilename !== false;
// if the file doesn't have a mime type, use a default since
// the HS errors if we don't supply one.
const contentType = opts.type || file.type || 'application/octet-stream';
@@ -214,7 +230,7 @@ module.exports.MatrixHttpApi.prototype = {
}
if (global.XMLHttpRequest) {
const defer = q.defer();
const defer = Promise.defer();
const xhr = new global.XMLHttpRequest();
upload.xhr = xhr;
const cb = requestCallback(defer, opts.callback, this.opts.onlyData);
@@ -255,13 +271,34 @@ module.exports.MatrixHttpApi.prototype = {
upload.loaded = ev.loaded;
upload.total = ev.total;
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
defer.notify(ev);
if (opts.progressHandler) {
opts.progressHandler({
loaded: ev.loaded,
total: ev.total,
});
}
});
let url = this.opts.baseUrl + "/_matrix/media/v1/upload";
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
url += "&filename=" + encodeURIComponent(fileName);
const queryArgs = [];
if (includeFilename && fileName) {
queryArgs.push("filename=" + encodeURIComponent(fileName));
}
if (!this.useAuthorizationHeader) {
queryArgs.push("access_token="
+ encodeURIComponent(this.opts.accessToken));
}
if (queryArgs.length > 0) {
url += "?" + queryArgs.join("&");
}
xhr.open("POST", url);
if (this.useAuthorizationHeader) {
xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken);
}
xhr.setRequestHeader("Content-Type", contentType);
xhr.send(body);
promise = defer.promise;
@@ -269,9 +306,11 @@ module.exports.MatrixHttpApi.prototype = {
// dirty hack (as per _request) to allow the upload to be cancelled.
promise.abort = xhr.abort.bind(xhr);
} else {
const queryParams = {
filename: fileName,
};
const queryParams = {};
if (includeFilename && fileName) {
queryParams.filename = fileName;
}
promise = this.authedRequest(
opts.callback, "POST", "/upload", queryParams, body, {
@@ -338,7 +377,7 @@ module.exports.MatrixHttpApi.prototype = {
opts.form = params;
}
const defer = q.defer();
const defer = Promise.defer();
this.opts.request(
opts,
requestCallback(defer, callback, this.opts.onlyData),
@@ -364,7 +403,8 @@ module.exports.MatrixHttpApi.prototype = {
*
* @param {Object} data The HTTP JSON body.
*
* @param {Object=} opts additional options
* @param {Object|Number=} opts additional options. If a number is specified,
* this is treated as `opts.localTimeoutMs`.
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
@@ -385,24 +425,51 @@ module.exports.MatrixHttpApi.prototype = {
if (!queryParams) {
queryParams = {};
}
if (!queryParams.access_token) {
queryParams.access_token = this.opts.accessToken;
if (this.useAuthorizationHeader) {
if (isFinite(opts)) {
// opts used to be localTimeoutMs
opts = {
localTimeoutMs: opts,
};
}
if (!opts) {
opts = {};
}
if (!opts.headers) {
opts.headers = {};
}
if (!opts.headers.Authorization) {
opts.headers.Authorization = "Bearer " + this.opts.accessToken;
}
if (queryParams.access_token) {
delete queryParams.access_token;
}
} else {
if (!queryParams.access_token) {
queryParams.access_token = this.opts.accessToken;
}
}
const request_promise = this.request(
const requestPromise = this.request(
callback, method, path, queryParams, data, opts,
);
const self = this;
request_promise.catch(function(err) {
requestPromise.catch(function(err) {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out");
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
self.event_emitter.emit(
"no_consent",
err.message,
err.data.consent_uri,
);
}
});
// return the original promise, otherwise tests break due to it having to
// go around the event loop one more time to process the result of the request
return request_promise;
return requestPromise;
},
/**
@@ -623,27 +690,58 @@ module.exports.MatrixHttpApi.prototype = {
}
}
const headers = utils.extend({}, opts.headers || {});
const json = opts.json === undefined ? true : opts.json;
let bodyParser = opts.bodyParser;
const defer = q.defer();
// we handle the json encoding/decoding here, because request and
// browser-request make a mess of it. Specifically, they attempt to
// json-decode plain-text error responses, which in turn means that the
// actual error gets swallowed by a SyntaxError.
if (json) {
if (data) {
data = JSON.stringify(data);
headers['content-type'] = 'application/json';
}
if (!headers['accept']) {
headers['accept'] = 'application/json';
}
if (bodyParser === undefined) {
bodyParser = function(rawBody) {
return JSON.parse(rawBody);
};
}
}
const defer = Promise.defer();
let timeoutId;
let timedOut = false;
let req;
const localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs;
if (localTimeoutMs) {
timeoutId = callbacks.setTimeout(function() {
timedOut = true;
if (req && req.abort) {
req.abort();
const resetTimeout = () => {
if (localTimeoutMs) {
if (timeoutId) {
callbacks.clearTimeout(timeoutId);
}
defer.reject(new module.exports.MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
timeout: localTimeoutMs,
}));
}, localTimeoutMs);
}
timeoutId = callbacks.setTimeout(function() {
timedOut = true;
if (req && req.abort) {
req.abort();
}
defer.reject(new module.exports.MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
timeout: localTimeoutMs,
}));
}, localTimeoutMs);
}
};
resetTimeout();
const reqPromise = defer.promise;
@@ -655,9 +753,9 @@ module.exports.MatrixHttpApi.prototype = {
withCredentials: false,
qs: queryParams,
body: data,
json: json,
json: false,
timeout: localTimeoutMs,
headers: opts.headers || {},
headers: headers || {},
_matrix_opts: this.opts,
},
function(err, response, body) {
@@ -668,21 +766,29 @@ module.exports.MatrixHttpApi.prototype = {
}
}
// if json is falsy, we won't parse any error response, so need
// to do so before turning it into a MatrixError
const parseErrorJson = !json;
const handlerFn = requestCallback(
defer, callback, self.opts.onlyData,
parseErrorJson,
opts.bodyParser,
bodyParser,
);
handlerFn(err, response, body);
},
);
if (req && req.abort) {
if (req) {
// This will only work in a browser, where opts.request is the
// `browser-request` import. Currently `request` does not support progress
// updates - see https://github.com/request/request/pull/2346.
// `browser-request` returns an XHRHttpRequest which exposes `onprogress`
if ('onprogress' in req) {
req.onprogress = (e) => {
// Prevent the timeout from rejecting the deferred promise if progress is
// seen with the request
resetTimeout();
};
}
// FIXME: This is EVIL, but I can't think of a better way to expose
// abort() operations on underlying HTTP requests :(
reqPromise.abort = req.abort.bind(req);
if (req.abort) reqPromise.abort = req.abort.bind(req);
}
} catch (ex) {
defer.reject(ex);
@@ -699,15 +805,18 @@ module.exports.MatrixHttpApi.prototype = {
* that will either resolve or reject the given defer as well as invoke the
* given userDefinedCallback (if any).
*
* If onlyData is true, the defer/callback is invoked with the body of the
* response, otherwise the result code.
* HTTP errors are transformed into javascript errors and the deferred is rejected.
*
* If parseErrorJson is true, we will JSON.parse the body if we get a 4xx error.
* If bodyParser is given, it is used to transform the body of the successful
* responses before passing to the defer/callback.
*
* If onlyData is true, the defer/callback is invoked with the body of the
* response, otherwise the result object (with `code` and `data` fields)
*
*/
const requestCallback = function(
defer, userDefinedCallback, onlyData,
parseErrorJson, bodyParser,
bodyParser,
) {
userDefinedCallback = userDefinedCallback || function() {};
@@ -715,19 +824,12 @@ const requestCallback = function(
if (!err) {
try {
if (response.statusCode >= 400) {
if (parseErrorJson) {
// we won't have json-decoded the response.
body = JSON.parse(body);
}
err = new module.exports.MatrixError(body);
err = parseErrorResponse(response, body);
} else if (bodyParser) {
body = bodyParser(body);
}
} catch (e) {
err = e;
}
if (err) {
err.httpStatus = response.statusCode;
err = new Error(`Error parsing server response: ${e}`);
}
}
@@ -737,6 +839,9 @@ const requestCallback = function(
} else {
const res = {
code: response.statusCode,
// XXX: why do we bother with this? it doesn't work for
// XMLHttpRequest, so clearly we don't use it.
headers: response.headers,
data: body,
};
@@ -746,6 +851,67 @@ const requestCallback = function(
};
};
/**
* Attempt to turn an HTTP error response into a Javascript Error.
*
* If it is a JSON response, we will parse it into a MatrixError. Otherwise
* we return a generic Error.
*
* @param {XMLHttpRequest|http.IncomingMessage} response response object
* @param {String} body raw body of the response
* @returns {Error}
*/
function parseErrorResponse(response, body) {
const httpStatus = response.statusCode;
const contentType = getResponseContentType(response);
let err;
if (contentType) {
if (contentType.type === 'application/json') {
err = new module.exports.MatrixError(JSON.parse(body));
} else if (contentType.type === 'text/plain') {
err = new Error(`Server returned ${httpStatus} error: ${body}`);
}
}
if (!err) {
err = new Error(`Server returned ${httpStatus} error`);
}
err.httpStatus = httpStatus;
return err;
}
/**
* extract the Content-Type header from the response object, and
* parse it to a `{type, parameters}` object.
*
* returns null if no content-type header could be found.
*
* @param {XMLHttpRequest|http.IncomingMessage} response response object
* @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found
*/
function getResponseContentType(response) {
let contentType;
if (response.getResponseHeader) {
// XMLHttpRequest provides getResponseHeader
contentType = response.getResponseHeader("Content-Type");
} else if (response.headers) {
// request provides http.IncomingMessage which has a message.headers map
contentType = response.headers['content-type'] || null;
}
if (!contentType) {
return null;
}
try {
return parseContentType(contentType);
} catch(e) {
throw new Error(`Error parsing Content-Type '${contentType}': ${e}`);
}
}
/**
* Construct a Matrix error. This is a JavaScript Error with additional
* information specific to the standard Matrix error response.
+24
View File
@@ -0,0 +1,24 @@
/*
Copyright 2017 Vector Creations 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.
*/
/**
* Separate exports file for the indexeddb web worker, which is designed
* to be used separately
*/
/** The {@link module:indexeddb-store-worker~IndexedDBStoreWorker} class. */
module.exports.IndexedDBStoreWorker = require("./store/indexeddb-store-worker.js");
+235 -32
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,10 +17,14 @@ limitations under the License.
"use strict";
/** @module interactive-auth */
const q = require("q");
import Promise from 'bluebird';
const url = require("url");
const utils = require("./utils");
const EMAIL_STAGE_TYPE = "m.login.email.identity";
const MSISDN_STAGE_TYPE = "m.login.msisdn";
/**
* Abstracts the logic used to drive the interactive auth process.
*
@@ -37,44 +42,123 @@ const utils = require("./utils");
*
* @param {object} opts options object
*
* @param {object} opts.matrixClient A matrix client to use for the auth process
*
* @param {object?} opts.authData error response from the last request. If
* null, a request will be made with no auth before starting.
*
* @param {function(object?): module:client.Promise} opts.doRequest
* called with the new auth dict to submit the request. Should return a
* @param {function(object?, bool?): module:client.Promise} opts.doRequest
* called with the new auth dict to submit the request and a flag set
* to true if this request is a background request. Should return a
* promise which resolves to the successful response or rejects with a
* MatrixError.
*
* @param {function(string, object?)} opts.startAuthStage
* called to ask the UI to start a particular auth stage. The arguments
* are: the login type (eg m.login.password); and (if the last request
* returned an error), an error object, with fields 'errcode' and 'error'.
* @param {function(string, object?)} opts.stateUpdated
* called when the status of the UI auth changes, ie. when the state of
* an auth stage changes of when the auth flow moves to a new stage.
* The arguments are: the login type (eg m.login.password); and an object
* which is either an error or an informational object specific to the
* login type. If the 'errcode' key is defined, the object is an error,
* and has keys:
* errcode: string, the textual error code, eg. M_UNKNOWN
* error: string, human readable string describing the error
*
* The login type specific objects are as follows:
* m.login.email.identity:
* * emailSid: string, the sid of the active email auth session
*
* @param {object?} opts.inputs Inputs provided by the user and used by different
* stages of the auto process. The inputs provided will affect what flow is chosen.
*
* @param {string?} opts.inputs.emailAddress An email address. If supplied, a flow
* using email verification will be chosen.
*
* @param {string?} opts.inputs.phoneCountry An ISO two letter country code. Gives
* the country that opts.phoneNumber should be resolved relative to.
*
* @param {string?} opts.inputs.phoneNumber A phone number. If supplied, a flow
* using phone number validation will be chosen.
*
* @param {string?} opts.sessionId If resuming an existing interactive auth session,
* the sessionId of that session.
*
* @param {string?} opts.clientSecret If resuming an existing interactive auth session,
* the client secret for that session
*
* @param {string?} opts.emailSid If returning from having completed m.login.email.identity
* auth, the sid for the email verification session.
*
*/
function InteractiveAuth(opts) {
this._data = opts.authData;
this._matrixClient = opts.matrixClient;
this._data = opts.authData || {};
this._requestCallback = opts.doRequest;
this._startAuthStageCallback = opts.startAuthStage;
// startAuthStage included for backwards compat
this._stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage;
this._completionDeferred = null;
this._inputs = opts.inputs || {};
if (opts.sessionId) this._data.session = opts.sessionId;
this._clientSecret = opts.clientSecret || this._matrixClient.generateClientSecret();
this._emailSid = opts.emailSid;
if (this._emailSid === undefined) this._emailSid = null;
this._currentStage = null;
}
InteractiveAuth.prototype = {
/**
* begin the authentication process.
*
* @return {module:client.Promise} which resolves to the response on success,
* or rejects with the error on failure.
* @return {module:client.Promise} which resolves to the response on success,
* or rejects with the error on failure. Rejects with NoAuthFlowFoundError if
* no suitable authentication flow can be found
*/
attemptAuth: function() {
this._completionDeferred = q.defer();
this._completionDeferred = Promise.defer();
if (!this._data) {
this._doRequest(null);
} else {
this._startNextAuthStage();
// wrap in a promise so that if _startNextAuthStage
// throws, it rejects the promise in a consistent way
return Promise.resolve().then(() => {
// if we have no flows, try a request (we'll have
// just a session ID in _data if resuming)
if (!this._data.flows) {
this._doRequest(this._data);
} else {
this._startNextAuthStage();
}
return this._completionDeferred.promise;
});
},
/**
* Poll to check if the auth session or current stage has been
* completed out-of-band. If so, the attemptAuth promise will
* be resolved.
*/
poll: function() {
if (!this._data.session) return;
let authDict = {};
if (this._currentStage == EMAIL_STAGE_TYPE) {
// The email can be validated out-of-band, but we need to provide the
// creds so the HS can go & check it.
if (this._emailSid) {
const idServerParsedUrl = url.parse(
this._matrixClient.getIdentityServerUrl(),
);
authDict = {
type: EMAIL_STAGE_TYPE,
threepid_creds: {
sid: this._emailSid,
client_secret: this._clientSecret,
id_server: idServerParsedUrl.host,
},
};
}
}
return this._completionDeferred.promise;
this.submitAuthDict(authDict, true);
},
/**
@@ -86,6 +170,16 @@ InteractiveAuth.prototype = {
return this._data ? this._data.session : undefined;
},
/**
* get the client secret used for validation sessions
* with the ID server.
*
* @return {string} client secret
*/
getClientSecret: function() {
return this._clientSecret;
},
/**
* get the server params for a given stage
*
@@ -108,8 +202,11 @@ InteractiveAuth.prototype = {
* @param {object} authData new auth dict to send to the server. Should
* include a `type` propterty denoting the login type, as well as any
* other params for that stage.
* @param {bool} background If true, this request failing will not result
* in the attemptAuth promise being rejected. This can be set to true
* for requests that just poll to see if auth has been completed elsewhere.
*/
submitAuthDict: function(authData) {
submitAuthDict: function(authData, background) {
if (!this._completionDeferred) {
throw new Error("submitAuthDict() called before attemptAuth()");
}
@@ -120,7 +217,29 @@ InteractiveAuth.prototype = {
};
utils.extend(auth, authData);
this._doRequest(auth);
this._doRequest(auth, background);
},
/**
* Gets the sid for the email validation session
* Specific to m.login.email.identity
*
* @returns {string} The sid of the email auth session
*/
getEmailSid: function() {
return this._emailSid;
},
/**
* Sets the sid for the email validation session
* This must be set in order to successfully poll for completion
* of the email validation.
* Specific to m.login.email.identity
*
* @param {string} sid The sid for the email validation session
*/
setEmailSid: function(sid) {
this._emailSid = sid;
},
/**
@@ -129,54 +248,98 @@ InteractiveAuth.prototype = {
*
* @private
* @param {object?} auth new auth dict, including session id
* @param {bool?} background If true, this request is a background poll, so it
* failing will not result in the attemptAuth promise being rejected.
* This can be set to true for requests that just poll to see if auth has
* been completed elsewhere.
*/
_doRequest: function(auth) {
_doRequest: function(auth, background) {
const self = this;
// hackery to make sure that synchronous exceptions end up in the catch
// handler (without the additional event loop entailed by q.fcall or an
// extra q().then)
// extra Promise.resolve().then)
let prom;
try {
prom = this._requestCallback(auth);
prom = this._requestCallback(auth, background);
} catch (e) {
prom = q.reject(e);
prom = Promise.reject(e);
}
prom.then(
prom = prom.then(
function(result) {
console.log("result from request: ", result);
self._completionDeferred.resolve(result);
}, function(error) {
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
// sometimes UI auth errors don't come with flows
const errorFlows = error.data ? error.data.flows : null;
const haveFlows = Boolean(self._data.flows) || Boolean(errorFlows);
if (error.httpStatus !== 401 || !error.data || !haveFlows) {
// doesn't look like an interactive-auth failure. fail the whole lot.
throw error;
}
// if the error didn't come with flows, completed flows or session ID,
// copy over the ones we have. Synapse sometimes sends responses without
// any UI auth data (eg. when polling for email validation, if the email
// has not yet been validated). This appears to be a Synapse bug, which
// we workaround here.
if (!error.data.flows && !error.data.completed && !error.data.session) {
error.data.flows = self._data.flows;
error.data.completed = self._data.completed;
error.data.session = self._data.session;
}
self._data = error.data;
self._startNextAuthStage();
},
).catch(this._completionDeferred.reject).done();
);
if (!background) {
prom = prom.catch((e) => {
this._completionDeferred.reject(e);
});
} else {
// We ignore all failures here (even non-UI auth related ones)
// since we don't want to suddenly fail if the internet connection
// had a blip whilst we were polling
prom = prom.catch((error) => {
console.log("Ignoring error from UI auth: " + error);
});
}
prom.done();
},
/**
* Pick the next stage and call the callback
*
* @private
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
_startNextAuthStage: function() {
const nextStage = this._chooseStage();
if (!nextStage) {
throw new Error("No incomplete flows from the server");
}
this._currentStage = nextStage;
if (nextStage == 'm.login.dummy') {
this.submitAuthDict({
type: 'm.login.dummy',
});
return;
}
let stageError = null;
if (this._data.errcode || this._data.error) {
stageError = {
this._stateUpdatedCallback(nextStage, {
errcode: this._data.errcode || "",
error: this._data.error || "",
};
});
return;
}
this._startAuthStageCallback(nextStage, stageError);
const stageStatus = {};
if (nextStage == EMAIL_STAGE_TYPE) {
stageStatus.emailSid = this._emailSid;
}
this._stateUpdatedCallback(nextStage, stageStatus);
},
/**
@@ -184,6 +347,7 @@ InteractiveAuth.prototype = {
*
* @private
* @return {string?} login type
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
_chooseStage: function() {
const flow = this._chooseFlow();
@@ -195,14 +359,53 @@ InteractiveAuth.prototype = {
/**
* Pick one of the flows from the returned list
* If a flow using all of the inputs is found, it will
* be returned, otherwise, null will be returned.
*
* Only flows using all given inputs are chosen because it
* is likley to be surprising if the user provides a
* credential and it is not used. For example, for registration,
* this could result in the email not being used which would leave
* the account with no means to reset a password.
*
* @private
* @return {object} flow
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
_chooseFlow: function() {
const flows = this._data.flows || [];
// always use the first flow for now
return flows[0];
// we've been given an email or we've already done an email part
const haveEmail = Boolean(this._inputs.emailAddress) || Boolean(this._emailSid);
const haveMsisdn = (
Boolean(this._inputs.phoneCountry) &&
Boolean(this._inputs.phoneNumber)
);
for (const flow of flows) {
let flowHasEmail = false;
let flowHasMsisdn = false;
for (const stage of flow.stages) {
if (stage === EMAIL_STAGE_TYPE) {
flowHasEmail = true;
} else if (stage == MSISDN_STAGE_TYPE) {
flowHasMsisdn = true;
}
}
if (flowHasEmail == haveEmail && flowHasMsisdn == haveMsisdn) {
return flow;
}
}
// Throw an error with a fairly generic description, but with more
// information such that the app can give a better one if so desired.
const err = new Error("No appropriate authentication flow found");
err.name = 'NoAuthFlowFoundError';
err.required_stages = [];
if (haveEmail) err.required_stages.push(EMAIL_STAGE_TYPE);
if (haveMsisdn) err.required_stages.push(MSISDN_STAGE_TYPE);
err.available_flows = flows;
throw err;
},
/**
+63
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,20 +16,32 @@ limitations under the License.
*/
"use strict";
/** The {@link module:ContentHelpers} object */
module.exports.ContentHelpers = require("./content-helpers");
/** The {@link module:models/event.MatrixEvent|MatrixEvent} class. */
module.exports.MatrixEvent = require("./models/event").MatrixEvent;
/** The {@link module:models/event.EventStatus|EventStatus} enum. */
module.exports.EventStatus = require("./models/event").EventStatus;
/** The {@link module:store/memory.MatrixInMemoryStore|MatrixInMemoryStore} class. */
module.exports.MatrixInMemoryStore = require("./store/memory").MatrixInMemoryStore;
/** The {@link module:store/indexeddb.IndexedDBStore|IndexedDBStore} class. */
module.exports.IndexedDBStore = require("./store/indexeddb").IndexedDBStore;
/** The {@link module:store/indexeddb.IndexedDBStoreBackend|IndexedDBStoreBackend} class. */
module.exports.IndexedDBStoreBackend = require("./store/indexeddb").IndexedDBStoreBackend;
/** The {@link module:sync-accumulator.SyncAccumulator|SyncAccumulator} class. */
module.exports.SyncAccumulator = require("./sync-accumulator");
/** The {@link module:http-api.MatrixHttpApi|MatrixHttpApi} class. */
module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
/** The {@link module:http-api.MatrixError|MatrixError} class. */
module.exports.MatrixError = require("./http-api").MatrixError;
/** The {@link module:errors.InvalidStoreError|InvalidStoreError} class. */
module.exports.InvalidStoreError = require("./errors").InvalidStoreError;
/** The {@link module:client.MatrixClient|MatrixClient} class. */
module.exports.MatrixClient = require("./client").MatrixClient;
/** The {@link module:models/room|Room} class. */
module.exports.Room = require("./models/room");
/** The {@link module:models/group|Group} class. */
module.exports.Group = require("./models/group");
/** The {@link module:models/event-timeline~EventTimeline} class. */
module.exports.EventTimeline = require("./models/event-timeline");
/** The {@link module:models/event-timeline-set~EventTimelineSet} class. */
@@ -56,6 +69,11 @@ module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
module.exports.InteractiveAuth = require("./interactive-auth");
module.exports.MemoryCryptoStore =
require("./crypto/store/memory-crypto-store").default;
module.exports.IndexedDBCryptoStore =
require("./crypto/store/indexeddb-crypto-store").default;
/**
* Create a new Matrix Call.
* @function
@@ -66,6 +84,30 @@ module.exports.InteractiveAuth = require("./interactive-auth");
*/
module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCall;
/**
* Set an audio output device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setMatrixCallAudioOutput = require('./webrtc/call').setAudioOutput;
/**
* Set an audio input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setMatrixCallAudioInput = require('./webrtc/call').setAudioInput;
/**
* Set a video input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setMatrixCallVideoInput = require('./webrtc/call').setVideoInput;
// expose the underlying request object so different environments can use
// different request libs (e.g. request or browser-request)
let request;
@@ -100,6 +142,19 @@ module.exports.wrapRequest = function(wrapper) {
};
};
let cryptoStoreFactory = () => new module.exports.MemoryCryptoStore;
/**
* Configure a different factory to be used for creating crypto stores
*
* @param {Function} fac a function which will return a new
* {@link module:crypto.store.base~CryptoStore}.
*/
module.exports.setCryptoStoreFactory = function(fac) {
cryptoStoreFactory = fac;
};
/**
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
@@ -112,6 +167,13 @@ module.exports.wrapRequest = function(wrapper) {
* {@link module:scheduler~MatrixScheduler}.
* @param {requestFunction} opts.request If not set, defaults to the function
* supplied to {@link request} which defaults to the request module from NPM.
*
* @param {module:crypto.store.base~CryptoStore=} opts.cryptoStore
* crypto store implementation. Calls the factory supplied to
* {@link setCryptoStoreFactory} if unspecified; or if no factory has been
* specified, uses a default implementation (indexeddb in the browser,
* in-memory otherwise).
*
* @return {MatrixClient} A new matrix client.
* @see {@link module:client~MatrixClient} for the full list of options for
* <code>opts</code>.
@@ -127,6 +189,7 @@ module.exports.createClient = function(opts) {
localStorage: global.localStorage,
});
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
opts.cryptoStore = opts.cryptoStore || cryptoStoreFactory();
return new module.exports.MatrixClient(opts);
};
+31 -22
View File
@@ -149,45 +149,53 @@ EventTimelineSet.prototype.replaceEventId = function(oldEventId, newEventId) {
* <p>This is used when /sync returns a 'limited' timeline.
*
* @param {string=} backPaginationToken token for back-paginating the new timeline
* @param {?bool} flush Whether to flush the non-live timelines too.
* @param {string=} forwardPaginationToken token for forward-paginating the old live timeline,
* if absent or null, all timelines are reset.
*
* @fires module:client~MatrixClient#event:"Room.timelineReset"
*/
EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flush) {
let newTimeline;
EventTimelineSet.prototype.resetLiveTimeline = function(
backPaginationToken, forwardPaginationToken,
) {
// Each EventTimeline has RoomState objects tracking the state at the start
// and end of that timeline. The copies at the end of the live timeline are
// special because they will have listeners attached to monitor changes to
// the current room state, so we move this RoomState from the end of the
// current live timeline to the end of the new one and, if necessary,
// replace it with a newly created one. We also make a copy for the start
// of the new timeline.
if (!this._timelineSupport || flush) {
// if timeline support is disabled, forget about the old timelines
newTimeline = new EventTimeline(this);
// if timeline support is disabled, forget about the old timelines
const resetAllTimelines = !this._timelineSupport || !forwardPaginationToken;
const oldTimeline = this._liveTimeline;
const newTimeline = resetAllTimelines ?
oldTimeline.forkLive(EventTimeline.FORWARDS) :
oldTimeline.fork(EventTimeline.FORWARDS);
if (resetAllTimelines) {
this._timelines = [newTimeline];
this._eventIdToTimeline = {};
} else {
newTimeline = this.addTimeline();
this._timelines.push(newTimeline);
}
// initialise the state in the new timeline from our last known state
const evMap = this._liveTimeline.getState(EventTimeline.FORWARDS).events;
const events = [];
for (const evtype in evMap) {
if (!evMap.hasOwnProperty(evtype)) {
continue;
}
for (const stateKey in evMap[evtype]) {
if (!evMap[evtype].hasOwnProperty(stateKey)) {
continue;
}
events.push(evMap[evtype][stateKey]);
}
if (forwardPaginationToken) {
// Now set the forward pagination token on the old live timeline
// so it can be forward-paginated.
oldTimeline.setPaginationToken(
forwardPaginationToken, EventTimeline.FORWARDS,
);
}
newTimeline.initialiseState(events);
// make sure we set the pagination token before firing timelineReset,
// otherwise clients which start back-paginating will fail, and then get
// stuck without realising that they *can* back-paginate.
newTimeline.setPaginationToken(backPaginationToken, EventTimeline.BACKWARDS);
// Now we can swap the live timeline to the new one.
this._liveTimeline = newTimeline;
this.emit("Room.timelineReset", this.room, this);
this.emit("Room.timelineReset", this.room, this, resetAllTimelines);
};
/**
@@ -655,4 +663,5 @@ module.exports = EventTimelineSet;
* @event module:client~MatrixClient#"Room.timelineReset"
* @param {Room} room The room whose live timeline was reset, if any
* @param {EventTimelineSet} timelineSet timelineSet room whose live timeline was reset
* @param {boolean} resetAllTimelines True if all timelines were reset.
*/
+75 -14
View File
@@ -1,3 +1,18 @@
/*
Copyright 2016, 2017 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
@@ -5,8 +20,6 @@
*/
const RoomState = require("./room-state");
const utils = require("../utils");
const MatrixEvent = require("./event").MatrixEvent;
/**
* Construct a new EventTimeline
@@ -73,22 +86,70 @@ EventTimeline.prototype.initialiseState = function(stateEvents) {
throw new Error("Cannot initialise state after events are added");
}
// we deep-copy the events here, in case they get changed later - we don't
// want changes to the start state leaking through to the end state.
const oldStateEvents = utils.map(
utils.deepCopy(
stateEvents.map(function(mxEvent) {
return mxEvent.event;
}),
),
function(ev) {
return new MatrixEvent(ev);
});
// We previously deep copied events here and used different copies in
// the oldState and state events: this decision seems to date back
// quite a way and was apparently made to fix a bug where modifications
// made to the start state leaked through to the end state.
// This really shouldn't be possible though: the events themselves should
// not change. Duplicating the events uses a lot of extra memory,
// so we now no longer do it. To assert that they really do never change,
// freeze them! Note that we can't do this for events in general:
// although it looks like the only things preventing us are the
// 'status' flag, forwardLooking (which is only set once when adding to the
// timeline) and possibly the sender (which seems like it should never be
// reset but in practice causes a lot of the tests to break).
for (const e of stateEvents) {
Object.freeze(e);
}
this._startState.setStateEvents(oldStateEvents);
this._startState.setStateEvents(stateEvents);
this._endState.setStateEvents(stateEvents);
};
/**
* Forks the (live) timeline, taking ownership of the existing directional state of this timeline.
* All attached listeners will keep receiving state updates from the new live timeline state.
* The end state of this timeline gets replaced with an independent copy of the current RoomState,
* and will need a new pagination token if it ever needs to paginate forwards.
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
* of the timeline.
*
* @return {EventTimeline} the new timeline
*/
EventTimeline.prototype.forkLive = function(direction) {
const forkState = this.getState(direction);
const timeline = new EventTimeline(this._eventTimelineSet);
timeline._startState = forkState.clone();
// Now clobber the end state of the new live timeline with that from the
// previous live timeline. It will be identical except that we'll keep
// using the same RoomMember objects for the 'live' set of members with any
// listeners still attached
timeline._endState = forkState;
// Firstly, we just stole the current timeline's end state, so it needs a new one.
// Make an immutable copy of the state so back pagination will get the correct sentinels.
this._endState = forkState.clone();
return timeline;
};
/**
* Creates an independent timeline, inheriting the directional state from this timeline.
*
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
* of the timeline.
*
* @return {EventTimeline} the new timeline
*/
EventTimeline.prototype.fork = function(direction) {
const forkState = this.getState(direction);
const timeline = new EventTimeline(this._eventTimelineSet);
timeline._startState = forkState.clone();
timeline._endState = forkState.clone();
return timeline;
};
/**
* Get the ID of the room for this timeline
* @return {string} room ID
+331 -68
View File
@@ -21,9 +21,9 @@ limitations under the License.
* @module models/event
*/
const EventEmitter = require("events").EventEmitter;
const utils = require('../utils.js');
import Promise from 'bluebird';
import {EventEmitter} from 'events';
import utils from '../utils.js';
/**
* Enum for event statuses.
@@ -49,6 +49,8 @@ module.exports.EventStatus = {
CANCELLED: "cancelled",
};
const interns = {};
/**
* Construct a Matrix Event object
* @constructor
@@ -61,7 +63,9 @@ module.exports.EventStatus = {
* from changes to event JSON between Matrix versions.
*
* @prop {RoomMember} sender The room member who sent this event, or null e.g.
* this is a presence event.
* this is a presence event. This is only guaranteed to be set for events that
* appear in a timeline, ie. do not guarantee that it will be set on state
* events.
* @prop {RoomMember} target The room member who is the target of this event, e.g.
* the invitee, the person being banned, etc.
* @prop {EventStatus} status The sending status of the event.
@@ -73,19 +77,69 @@ module.exports.EventStatus = {
module.exports.MatrixEvent = function MatrixEvent(
event,
) {
// intern the values of matrix events to force share strings and reduce the
// amount of needless string duplication. This can save moderate amounts of
// memory (~10% on a 350MB heap).
// 'membership' at the event level (rather than the content level) is a legacy
// field that Riot never otherwise looks at, but it will still take up a lot
// of space if we don't intern it.
["state_key", "type", "sender", "room_id", "membership"].forEach((prop) => {
if (!event[prop]) {
return;
}
if (!interns[event[prop]]) {
interns[event[prop]] = event[prop];
}
event[prop] = interns[event[prop]];
});
["membership", "avatar_url", "displayname"].forEach((prop) => {
if (!event.content || !event.content[prop]) {
return;
}
if (!interns[event.content[prop]]) {
interns[event.content[prop]] = event.content[prop];
}
event.content[prop] = interns[event.content[prop]];
});
this.event = event || {};
this.sender = null;
this.target = null;
this.status = null;
this.error = null;
this.forwardLooking = true;
this._pushActions = null;
this._date = this.event.origin_server_ts ?
new Date(this.event.origin_server_ts) : null;
this._clearEvent = {};
this._keysProved = {};
this._keysClaimed = {};
/* curve25519 key which we believe belongs to the sender of the event. See
* getSenderKey()
*/
this._senderCurve25519Key = null;
/* ed25519 key which the sender of this event (for olm) or the creator of
* the megolm session (for megolm) claims to own. See getClaimedEd25519Key()
*/
this._claimedEd25519Key = null;
/* curve25519 keys of devices involved in telling us about the
* _senderCurve25519Key and _claimedEd25519Key.
* See getForwardingCurve25519KeyChain().
*/
this._forwardingCurve25519KeyChain = [];
/* if we have a process decrypting this event, a Promise which resolves
* when it is finished. Normally null.
*/
this._decryptionPromise = null;
/* flag to indicate if we should retry decrypting this event after the
* first attempt (eg, we have received new data which means that a second
* attempt may succeed)
*/
this._retryDecryption = false;
};
utils.inherits(module.exports.MatrixEvent, EventEmitter);
@@ -151,7 +205,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
* @return {Date} The event date, e.g. <code>new Date(1433502692297)</code>
*/
getDate: function() {
return this._date;
return this.event.origin_server_ts ? new Date(this.event.origin_server_ts) : null;
},
/**
@@ -233,9 +287,18 @@ utils.extend(module.exports.MatrixEvent.prototype, {
* <tt>"m.room.encrypted"</tt>
*
* @param {object} crypto_content raw 'content' for the encrypted event.
* @param {object} keys The local keys claimed and proved by this event.
*
* @param {string} senderCurve25519Key curve25519 key to record for the
* sender of this event.
* See {@link module:models/event.MatrixEvent#getSenderKey}.
*
* @param {string} claimedEd25519Key claimed ed25519 key to record for the
* sender if this event.
* See {@link module:models/event.MatrixEvent#getClaimedEd25519Key}
*/
makeEncrypted: function(crypto_type, crypto_content, keys) {
makeEncrypted: function(
crypto_type, crypto_content, senderCurve25519Key, claimedEd25519Key,
) {
// keep the plain-text data for 'view source'
this._clearEvent = {
type: this.event.type,
@@ -243,8 +306,187 @@ utils.extend(module.exports.MatrixEvent.prototype, {
};
this.event.type = crypto_type;
this.event.content = crypto_content;
this._keysProved = keys;
this._keysClaimed = keys;
this._senderCurve25519Key = senderCurve25519Key;
this._claimedEd25519Key = claimedEd25519Key;
},
/**
* Check if this event is currently being decrypted.
*
* @return {boolean} True if this event is currently being decrypted, else false.
*/
isBeingDecrypted: function() {
return this._decryptionPromise != null;
},
/**
* Check if this event is an encrypted event which we failed to decrypt
*
* (This implies that we might retry decryption at some point in the future)
*
* @return {boolean} True if this event is an encrypted event which we
* couldn't decrypt.
*/
isDecryptionFailure: function() {
return this._clearEvent && this._clearEvent.content &&
this._clearEvent.content.msgtype === "m.bad.encrypted";
},
/**
* Start the process of trying to decrypt this event.
*
* (This is used within the SDK: it isn't intended for use by applications)
*
* @internal
*
* @param {module:crypto} crypto crypto module
*
* @returns {Promise} promise which resolves (to undefined) when the decryption
* attempt is completed.
*/
attemptDecryption: async function(crypto) {
// start with a couple of sanity checks.
if (!this.isEncrypted()) {
throw new Error("Attempt to decrypt event which isn't encrypted");
}
if (
this._clearEvent && this._clearEvent.content &&
this._clearEvent.content.msgtype !== "m.bad.encrypted"
) {
// we may want to just ignore this? let's start with rejecting it.
throw new Error(
"Attempt to decrypt event which has already been encrypted",
);
}
// if we already have a decryption attempt in progress, then it may
// fail because it was using outdated info. We now have reason to
// succeed where it failed before, but we don't want to have multiple
// attempts going at the same time, so just set a flag that says we have
// new info.
//
if (this._decryptionPromise) {
console.log(
`Event ${this.getId()} already being decrypted; queueing a retry`,
);
this._retryDecryption = true;
return this._decryptionPromise;
}
this._decryptionPromise = this._decryptionLoop(crypto);
return this._decryptionPromise;
},
/**
* Cancel any room key request for this event and resend another.
*
* @param {module:crypto} crypto crypto module
*/
cancelAndResendKeyRequest: function(crypto) {
const wireContent = this.getWireContent();
crypto.cancelRoomKeyRequest({
algorithm: wireContent.algorithm,
room_id: this.getRoomId(),
session_id: wireContent.session_id,
sender_key: wireContent.sender_key,
}, true);
},
_decryptionLoop: async function(crypto) {
// make sure that this method never runs completely synchronously.
// (doing so would mean that we would clear _decryptionPromise *before*
// it is set in attemptDecryption - and hence end up with a stuck
// `_decryptionPromise`).
await Promise.resolve();
while (true) {
this._retryDecryption = false;
let res;
let err;
try {
if (!crypto) {
res = this._badEncryptedMessage("Encryption not enabled");
} else {
res = await crypto.decryptEvent(this);
}
} catch (e) {
if (e.name !== "DecryptionError") {
// not a decryption error: log the whole exception as an error
// (and don't bother with a retry)
console.error(
`Error decrypting event (id=${this.getId()}): ${e.stack || e}`,
);
this._decryptionPromise = null;
this._retryDecryption = false;
return;
}
err = e;
// see if we have a retry queued.
//
// NB: make sure to keep this check in the same tick of the
// event loop as `_decryptionPromise = null` below - otherwise we
// risk a race:
//
// * A: we check _retryDecryption here and see that it is
// false
// * B: we get a second call to attemptDecryption, which sees
// that _decryptionPromise is set so sets
// _retryDecryption
// * A: we continue below, clear _decryptionPromise, and
// never do the retry.
//
if (this._retryDecryption) {
// decryption error, but we have a retry queued.
console.log(
`Got error decrypting event (id=${this.getId()}: ` +
`${e}), but retrying`,
);
continue;
}
// decryption error, no retries queued. Warn about the error and
// set it to m.bad.encrypted.
console.warn(
`Error decrypting event (id=${this.getId()}): ${e.detailedString}`,
);
res = this._badEncryptedMessage(e.message);
}
// at this point, we've either successfully decrypted the event, or have given up
// (and set res to a 'badEncryptedMessage'). Either way, we can now set the
// cleartext of the event and raise Event.decrypted.
//
// make sure we clear '_decryptionPromise' before sending the 'Event.decrypted' event,
// otherwise the app will be confused to see `isBeingDecrypted` still set when
// there isn't an `Event.decrypted` on the way.
//
// see also notes on _retryDecryption above.
//
this._decryptionPromise = null;
this._retryDecryption = false;
this._setClearData(res);
this.emit("Event.decrypted", this, err);
return;
}
},
_badEncryptedMessage: function(reason) {
return {
clearEvent: {
type: "m.room.message",
content: {
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: " + reason + " **",
},
},
};
},
/**
@@ -256,20 +498,17 @@ utils.extend(module.exports.MatrixEvent.prototype, {
*
* @fires module:models/event.MatrixEvent#"Event.decrypted"
*
* @param {Object} clearEvent The plaintext payload for the event
* (typically containing <tt>type</tt> and <tt>content</tt> fields).
*
* @param {Object=} keysProved Keys owned by the sender of this event.
* See {@link module:models/event.MatrixEvent#getKeysProved}.
*
* @param {Object=} keysClaimed Keys the sender of this event claims.
* See {@link module:models/event.MatrixEvent#getKeysClaimed}.
* @param {module:crypto~EventDecryptionResult} decryptionResult
* the decryption result, including the plaintext and some key info
*/
setClearData: function(clearEvent, keysProved, keysClaimed) {
this._clearEvent = clearEvent;
this._keysProved = keysProved || {};
this._keysClaimed = keysClaimed || {};
this.emit("Event.decrypted", this);
_setClearData: function(decryptionResult) {
this._clearEvent = decryptionResult.clearEvent;
this._senderCurve25519Key =
decryptionResult.senderCurve25519Key || null;
this._claimedEd25519Key =
decryptionResult.claimedEd25519Key || null;
this._forwardingCurve25519KeyChain =
decryptionResult.forwardingCurve25519KeyChain || [];
},
/**
@@ -281,37 +520,72 @@ utils.extend(module.exports.MatrixEvent.prototype, {
},
/**
* The curve25519 key that sent this event
* The curve25519 key for the device that we think sent this event
*
* For an Olm-encrypted event, this is inferred directly from the DH
* exchange at the start of the session: the curve25519 key is involved in
* the DH exchange, so only a device which holds the private part of that
* key can establish such a session.
*
* For a megolm-encrypted event, it is inferred from the Olm message which
* established the megolm session
*
* @return {string}
*/
getSenderKey: function() {
return this.getKeysProved().curve25519 || null;
},
/**
* The keys that must have been owned by the sender of this encrypted event.
* <p>
* These don't necessarily have to come from this event itself, but may be
* implied by the cryptographic session.
*
* @return {Object<string, string>}
*/
getKeysProved: function() {
return this._keysProved;
return this._senderCurve25519Key;
},
/**
* The additional keys the sender of this encrypted event claims to possess.
* <p>
* These don't necessarily have to come from this event itself, but may be
* implied by the cryptographic session.
* For example megolm messages don't claim keys directly, but instead
* inherit a claim from the olm message that established the session.
*
* Just a wrapper for #getClaimedEd25519Key (q.v.)
*
* @return {Object<string, string>}
*/
getKeysClaimed: function() {
return this._keysClaimed;
return {
ed25519: this._claimedEd25519Key,
};
},
/**
* Get the ed25519 the sender of this event claims to own.
*
* For Olm messages, this claim is encoded directly in the plaintext of the
* event itself. For megolm messages, it is implied by the m.room_key event
* which established the megolm session.
*
* Until we download the device list of the sender, it's just a claim: the
* device list gives a proof that the owner of the curve25519 key used for
* this event (and returned by #getSenderKey) also owns the ed25519 key by
* signing the public curve25519 key with the ed25519 key.
*
* In general, applications should not use this method directly, but should
* instead use MatrixClient.getEventSenderDeviceInfo.
*
* @return {string}
*/
getClaimedEd25519Key: function() {
return this._claimedEd25519Key;
},
/**
* Get the curve25519 keys of the devices which were involved in telling us
* about the claimedEd25519Key and sender curve25519 key.
*
* Normally this will be empty, but in the case of a forwarded megolm
* session, the sender keys are sent to us by another device (the forwarding
* device), which we need to trust to do this. In that case, the result will
* be a list consisting of one entry.
*
* If the device that sent us the key (A) got it from another device which
* it wasn't prepared to vouch for (B), the result will be [A, B]. And so on.
*
* @return {string[]} base64-encoded curve25519 keys, from oldest to newest.
*/
getForwardingCurve25519KeyChain: function() {
return this._forwardingCurve25519KeyChain;
},
getUnsigned: function() {
@@ -399,36 +673,22 @@ utils.extend(module.exports.MatrixEvent.prototype, {
this.event = event;
// successfully sent.
this.status = null;
this._date = new Date(this.event.origin_server_ts);
},
});
/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says:
/* _REDACT_KEEP_KEY_MAP gives the keys we keep when an event is redacted
*
* the server should strip off any keys not in the following list:
* event_id
* type
* room_id
* user_id
* state_key
* prev_state
* content
* [we keep 'unsigned' as well, since that is created by the local server]
* This is specified here:
* http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#redactions
*
* The content object should also be stripped of all keys, unless it is one of
* one of the following event types:
* m.room.member allows key membership
* m.room.create allows key creator
* m.room.join_rules allows key join_rule
* m.room.power_levels allows keys ban, events, events_default, kick,
* redact, state_default, users, users_default.
* m.room.aliases allows key aliases
* Also:
* - We keep 'unsigned' since that is created by the local server
* - We keep user_id for backwards-compat with v1
*/
// a map giving the keys we keep when an event is redacted
const _REDACT_KEEP_KEY_MAP = [
'event_id', 'type', 'room_id', 'user_id', 'state_key', 'prev_state',
'content', 'unsigned',
'event_id', 'type', 'room_id', 'user_id', 'sender', 'state_key', 'prev_state',
'content', 'unsigned', 'origin_server_ts',
].reduce(function(ret, val) {
ret[val] = 1; return ret;
}, {});
@@ -453,4 +713,7 @@ const _REDACT_KEEP_CONTENT_MAP = {
*
* @param {module:models/event.MatrixEvent} event
* The matrix event which has been decrypted
* @param {module:crypto/algorithms/base.DecryptionError?} err
* The error that occured during decryption, or `undefined` if no
* error occured.
*/
+95
View File
@@ -0,0 +1,95 @@
/*
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 models/group
*/
const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
/**
* Construct a new Group.
*
* @param {string} groupId The ID of this group.
*
* @prop {string} groupId The ID of this group.
* @prop {string} name The human-readable display name for this group.
* @prop {string} avatarUrl The mxc URL for this group's avatar.
* @prop {string} myMembership The logged in user's membership of this group
* @prop {Object} inviter Infomation about the user who invited the logged in user
* to the group, if myMembership is 'invite'.
* @prop {string} inviter.userId The user ID of the inviter
*/
function Group(groupId) {
this.groupId = groupId;
this.name = null;
this.avatarUrl = null;
this.myMembership = null;
this.inviter = null;
}
utils.inherits(Group, EventEmitter);
Group.prototype.setProfile = function(name, avatarUrl) {
if (this.name === name && this.avatarUrl === avatarUrl) return;
this.name = name || this.groupId;
this.avatarUrl = avatarUrl;
this.emit("Group.profile", this);
};
Group.prototype.setMyMembership = function(membership) {
if (this.myMembership === membership) return;
this.myMembership = membership;
this.emit("Group.myMembership", this);
};
/**
* Sets the 'inviter' property. This does not emit an event (the inviter
* will only change when the user is revited / reinvited to a room),
* so set this before setting myMembership.
* @param {Object} inviter Infomation about who invited us to the room
*/
Group.prototype.setInviter = function(inviter) {
this.inviter = inviter;
};
module.exports = Group;
/**
* Fires whenever a group's profile information is updated.
* This means the 'name' and 'avatarUrl' properties.
* @event module:client~MatrixClient#"Group.profile"
* @param {Group} group The group whose profile was updated.
* @example
* matrixClient.on("Group.profile", function(group){
* var name = group.name;
* });
*/
/**
* Fires whenever the logged in user's membership status of
* the group is updated.
* @event module:client~MatrixClient#"Group.myMembership"
* @param {Group} group The group in which the user's membership changed
* @example
* matrixClient.on("Group.myMembership", function(group){
* var myMembership = group.myMembership;
* });
*/
+116 -20
View File
@@ -33,7 +33,10 @@ const utils = require("../utils");
* @prop {string} roomId The room ID for this member.
* @prop {string} userId The user ID of this member.
* @prop {boolean} typing True if the room member is currently typing.
* @prop {string} name The human-readable name for this room member.
* @prop {string} name The human-readable name for this room member. This will be
* disambiguated with a suffix of " (@user_id:matrix.org)" if another member shares the
* same displayname.
* @prop {string} rawDisplayName The ambiguous displayname of this room member.
* @prop {Number} powerLevel The power level for this room member.
* @prop {Number} powerLevelNorm The normalised power level (0-100) for this
* room member.
@@ -47,6 +50,7 @@ function RoomMember(roomId, userId) {
this.userId = userId;
this.typing = false;
this.name = userId;
this.rawDisplayName = userId;
this.powerLevel = 0;
this.powerLevelNorm = 0;
this.user = null;
@@ -54,10 +58,27 @@ function RoomMember(roomId, userId) {
this.events = {
member: null,
};
this._isOutOfBand = false;
this._updateModifiedTime();
}
utils.inherits(RoomMember, EventEmitter);
/**
* Mark the member as coming from a channel that is not sync
*/
RoomMember.prototype.markOutOfBand = function() {
this._isOutOfBand = true;
};
/**
* @return {bool} does the member come from a channel that is not sync?
* This is used to store the member seperately
* from the sync state so it available across browser sessions.
*/
RoomMember.prototype.isOutOfBand = function() {
return this._isOutOfBand;
};
/**
* Update this room member's membership event. May fire "RoomMember.name" if
* this event updates this member's name.
@@ -71,13 +92,21 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) {
if (event.getType() !== "m.room.member") {
return;
}
this._isOutOfBand = false;
this.events.member = event;
const oldMembership = this.membership;
this.membership = event.getDirectionalContent().membership;
const oldName = this.name;
this.name = calculateDisplayName(this, event, roomState);
this.name = calculateDisplayName(
this.userId,
event.getDirectionalContent().displayname,
roomState);
this.rawDisplayName = event.getDirectionalContent().displayname || this.userId;
if (oldMembership !== this.membership) {
this._updateModifiedTime();
this.emit("RoomMember.membership", event, this, oldMembership);
@@ -99,17 +128,20 @@ RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) {
if (powerLevelEvent.getType() !== "m.room.power_levels") {
return;
}
let maxLevel = powerLevelEvent.getContent().users_default || 0;
utils.forEach(utils.values(powerLevelEvent.getContent().users), function(lvl) {
const evContent = powerLevelEvent.getDirectionalContent();
let maxLevel = evContent.users_default || 0;
utils.forEach(utils.values(evContent.users), function(lvl) {
maxLevel = Math.max(maxLevel, lvl);
});
const oldPowerLevel = this.powerLevel;
const oldPowerLevelNorm = this.powerLevelNorm;
if (powerLevelEvent.getContent().users[this.userId] !== undefined) {
this.powerLevel = powerLevelEvent.getContent().users[this.userId];
} else if (powerLevelEvent.getContent().users_default !== undefined) {
this.powerLevel = powerLevelEvent.getContent().users_default;
if (evContent.users && evContent.users[this.userId] !== undefined) {
this.powerLevel = evContent.users[this.userId];
} else if (evContent.users_default !== undefined) {
this.powerLevel = evContent.users_default;
} else {
this.powerLevel = 0;
}
@@ -169,6 +201,44 @@ RoomMember.prototype.getLastModifiedTime = function() {
return this._modified;
};
RoomMember.prototype.isKicked = function() {
return this.membership === "leave" &&
this.events.member.getSender() !== this.events.member.getStateKey();
};
/**
* If this member was invited with the is_direct flag set, return
* the user that invited this member
* @return {string} user id of the inviter
*/
RoomMember.prototype.getDMInviter = function() {
// when not available because that room state hasn't been loaded in,
// we don't really know, but more likely to not be a direct chat
if (this.events.member) {
// TODO: persist the is_direct flag on the member as more member events
// come in caused by displayName changes.
// the is_direct flag is set on the invite member event.
// This is copied on the prev_content section of the join member event
// when the invite is accepted.
const memberEvent = this.events.member;
let memberContent = memberEvent.getContent();
let inviteSender = memberEvent.getSender();
if (memberContent.membership === "join") {
memberContent = memberEvent.getPrevContent();
inviteSender = memberEvent.getUnsigned().prev_sender;
}
if (memberContent.membership === "invite" && memberContent.is_direct) {
return inviteSender;
}
}
};
/**
* Get the avatar URL for a room member.
* @param {string} baseUrl The base homeserver URL See
@@ -192,10 +262,12 @@ RoomMember.prototype.getAvatarUrl =
if (allowDefault === undefined) {
allowDefault = true;
}
if (!this.events.member && !allowDefault) {
const rawUrl = this.getMxcAvatarUrl();
if (!rawUrl && !allowDefault) {
return null;
}
const rawUrl = this.events.member ? this.events.member.getContent().avatar_url : null;
const httpUrl = ContentRepo.getHttpUriForMxc(
baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks,
);
@@ -208,12 +280,21 @@ RoomMember.prototype.getAvatarUrl =
}
return null;
};
/**
* get the mxc avatar url, either from a state event, or from a lazily loaded member
* @return {string} the mxc avatar url
*/
RoomMember.prototype.getMxcAvatarUrl = function() {
if(this.events.member) {
return this.events.member.getDirectionalContent().avatar_url;
} else if(this.user) {
return this.user.avatarUrl;
}
return null;
};
function calculateDisplayName(member, event, roomState) {
const displayName = event.getDirectionalContent().displayname;
const selfUserId = member.userId;
if (!displayName) {
function calculateDisplayName(selfUserId, displayName, roomState) {
if (!displayName || displayName === selfUserId) {
return selfUserId;
}
@@ -221,11 +302,26 @@ function calculateDisplayName(member, event, roomState) {
return displayName;
}
const userIds = roomState.getUserIdsWithDisplayName(displayName);
const otherUsers = userIds.filter(function(u) {
return u !== selfUserId;
});
if (otherUsers.length > 0) {
// First check if the displayname is something we consider truthy
// after stripping it of zero width characters and padding spaces
const strippedDisplayName = utils.removeHiddenChars(displayName);
if (!strippedDisplayName) {
return selfUserId;
}
// Next check if the name contains something that look like a mxid
// If it does, it may be someone trying to impersonate someone else
// Show full mxid in this case
// Also show mxid if there are other people with the same displayname
// ignoring any zero width chars (unicode 200B-200D)
// if their displayname is made up of just zero width chars, show full mxid
let disambiguate = /@.+:.+/.test(displayName);
if (!disambiguate) {
const userIds = roomState.getUserIdsWithDisplayName(strippedDisplayName);
disambiguate = userIds.some((u) => u !== selfUserId);
}
if (disambiguate) {
return displayName + " (" + selfUserId + ")";
}
return displayName;
+410 -49
View File
@@ -22,18 +22,46 @@ const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
const RoomMember = require("./room-member");
// possible statuses for out-of-band member loading
const OOB_STATUS_NOTSTARTED = 1;
const OOB_STATUS_INPROGRESS = 2;
const OOB_STATUS_FINISHED = 3;
/**
* Construct room state.
*
* Room State represents the state of the room at a given point.
* It can be mutated by adding state events to it.
* There are two types of room member associated with a state event:
* normal member objects (accessed via getMember/getMembers) which mutate
* with the state to represent the current state of that room/user, eg.
* the object returned by getMember('@bob:example.com') will mutate to
* get a different display name if Bob later changes his display name
* in the room.
* There are also 'sentinel' members (accessed via getSentinelMember).
* These also represent the state of room members at the point in time
* represented by the RoomState object, but unlike objects from getMember,
* sentinel objects will always represent the room state as at the time
* getSentinelMember was called, so if Bob subsequently changes his display
* name, a room member object previously acquired with getSentinelMember
* will still have his old display name. Calling getSentinelMember again
* after the display name change will return a new RoomMember object
* with Bob's new display name.
*
* @constructor
* @param {?string} roomId Optional. The ID of the room which has this state.
* If none is specified it just tracks paginationTokens, useful for notifTimelineSet
* @param {?object} oobMemberFlags Optional. The state of loading out of bound members.
* As the timeline might get reset while they are loading, this state needs to be inherited
* and shared when the room state is cloned for the new timeline.
* This should only be passed from clone.
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
* on the user's ID.
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
* events dictionary, keyed on the event type and then the state_key value.
* @prop {string} paginationToken The pagination token for this state.
*/
function RoomState(roomId) {
function RoomState(roomId, oobMemberFlags = undefined) {
this.roomId = roomId;
this.members = {
// userId: RoomMember
@@ -50,9 +78,74 @@ function RoomState(roomId) {
this._displayNameToUserIds = {};
this._userIdsToDisplayNames = {};
this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite
this._joinedMemberCount = null; // cache of the number of joined members
// joined members count from summary api
// once set, we know the server supports the summary api
// and we should only trust that
// we could also only trust that before OOB members
// are loaded but doesn't seem worth the hassle atm
this._summaryJoinedMemberCount = null;
// same for invited member count
this._invitedMemberCount = null;
this._summaryInvitedMemberCount = null;
if (!oobMemberFlags) {
oobMemberFlags = {
status: OOB_STATUS_NOTSTARTED,
};
}
this._oobMemberFlags = oobMemberFlags;
}
utils.inherits(RoomState, EventEmitter);
/**
* Returns the number of joined members in this room
* This method caches the result.
* @return {integer} The number of members in this room whose membership is 'join'
*/
RoomState.prototype.getJoinedMemberCount = function() {
if (this._summaryJoinedMemberCount !== null) {
return this._summaryJoinedMemberCount;
}
if (this._joinedMemberCount === null) {
this._joinedMemberCount = this.getMembers().reduce((count, m) => {
return m.membership === 'join' ? count + 1 : count;
}, 0);
}
return this._joinedMemberCount;
};
/**
* Set the joined member count explicitly (like from summary part of the sync response)
* @param {number} count the amount of joined members
*/
RoomState.prototype.setJoinedMemberCount = function(count) {
this._summaryJoinedMemberCount = count;
};
/**
* Returns the number of invited members in this room
* @return {integer} The number of members in this room whose membership is 'invite'
*/
RoomState.prototype.getInvitedMemberCount = function() {
if (this._summaryInvitedMemberCount !== null) {
return this._summaryInvitedMemberCount;
}
if (this._invitedMemberCount === null) {
this._invitedMemberCount = this.getMembers().reduce((count, m) => {
return m.membership === 'invite' ? count + 1 : count;
}, 0);
}
return this._invitedMemberCount;
};
/**
* Set the amount of invited members in this room
* @param {number} count the amount of invited members
*/
RoomState.prototype.setInvitedMemberCount = function(count) {
this._summaryInvitedMemberCount = count;
};
/**
* Get all RoomMembers in this room.
* @return {Array<RoomMember>} A list of RoomMembers.
@@ -80,7 +173,18 @@ RoomState.prototype.getMember = function(userId) {
* @return {RoomMember} The member or null if they do not exist.
*/
RoomState.prototype.getSentinelMember = function(userId) {
return this._sentinels[userId] || null;
if (!userId) return null;
let sentinel = this._sentinels[userId];
if (sentinel === undefined) {
sentinel = new RoomMember(this.roomId, userId);
const member = this.members[userId];
if (member) {
sentinel.setMembershipEvent(member.events.member, this);
}
this._sentinels[userId] = sentinel;
}
return sentinel;
};
/**
@@ -104,6 +208,67 @@ RoomState.prototype.getStateEvents = function(eventType, stateKey) {
return event ? event : null;
};
/**
* Creates a copy of this room state so that mutations to either won't affect the other.
* @return {RoomState} the copy of the room state
*/
RoomState.prototype.clone = function() {
const copy = new RoomState(this.roomId, this._oobMemberFlags);
// Ugly hack: because setStateEvents will mark
// members as susperseding future out of bound members
// if loading is in progress (through _oobMemberFlags)
// since these are not new members, we're merely copying them
// set the status to not started
// after copying, we set back the status
const status = this._oobMemberFlags.status;
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
Object.values(this.events).forEach((eventsByStateKey) => {
const eventsForType = Object.values(eventsByStateKey);
copy.setStateEvents(eventsForType);
});
// Ugly hack: see above
this._oobMemberFlags.status = status;
if (this._summaryInvitedMemberCount !== null) {
copy.setInvitedMemberCount(this.getInvitedMemberCount());
}
if (this._summaryJoinedMemberCount !== null) {
copy.setJoinedMemberCount(this.getJoinedMemberCount());
}
// copy out of band flags if needed
if (this._oobMemberFlags.status == OOB_STATUS_FINISHED) {
// copy markOutOfBand flags
this.getMembers().forEach((member) => {
if (member.isOutOfBand()) {
const copyMember = copy.getMember(member.userId);
copyMember.markOutOfBand();
}
});
}
return copy;
};
/**
* Add previously unknown state events.
* When lazy loading members while back-paginating,
* the relevant room state for the timeline chunk at the end
* of the chunk can be set with this method.
* @param {MatrixEvent[]} events state events to prepend
*/
RoomState.prototype.setUnknownStateEvents = function(events) {
const unknownStateEvents = events.filter((event) => {
return this.events[event.getType()] === undefined ||
this.events[event.getType()][event.getStateKey()] === undefined;
});
this.setStateEvents(unknownStateEvents);
};
/**
* Add an array of one or more state MatrixEvents, overwriting
* any existing state with the same {type, stateKey} tuple. Will fire
@@ -127,10 +292,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
return;
}
if (self.events[event.getType()] === undefined) {
self.events[event.getType()] = {};
}
self.events[event.getType()][event.getStateKey()] = event;
self._setStateEvent(event);
if (event.getType() === "m.room.member") {
_updateDisplayNameCache(
self, event.getStateKey(), event.getContent().displayname,
@@ -168,28 +330,10 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
event.getPrevContent().displayname;
}
let member = self.members[userId];
if (!member) {
member = new RoomMember(event.getRoomId(), userId);
self.emit("RoomState.newMember", event, self, member);
}
// Add a new sentinel for this change. We apply the same
// operations to both sentinel and member rather than deep copying
// so we don't make assumptions about the properties of RoomMember
// (e.g. and manage to break it because deep copying doesn't do
// everything).
const sentinel = new RoomMember(event.getRoomId(), userId);
utils.forEach([member, sentinel], function(roomMember) {
roomMember.setMembershipEvent(event, self);
// this member may have a power level already, so set it.
const pwrLvlEvent = self.getStateEvents("m.room.power_levels", "");
if (pwrLvlEvent) {
roomMember.setPowerLevelEvent(pwrLvlEvent);
}
});
const member = self._getOrCreateMember(userId, event);
member.setMembershipEvent(event, self);
self._sentinels[userId] = sentinel;
self.members[userId] = member;
self._updateMember(member);
self.emit("RoomState.members", event, self, member);
} else if (event.getType() === "m.room.power_levels") {
const members = utils.values(self.members);
@@ -197,10 +341,147 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
member.setPowerLevelEvent(event);
self.emit("RoomState.members", event, self, member);
});
// assume all our sentinels are now out-of-date
self._sentinels = {};
}
});
};
/**
* Looks up a member by the given userId, and if it doesn't exist,
* create it and emit the `RoomState.newMember` event.
* This method makes sure the member is added to the members dictionary
* before emitting, as this is done from setStateEvents and _setOutOfBandMember.
* @param {string} userId the id of the user to look up
* @param {MatrixEvent} event the membership event for the (new) member. Used to emit.
* @fires module:client~MatrixClient#event:"RoomState.newMember"
* @returns {RoomMember} the member, existing or newly created.
*/
RoomState.prototype._getOrCreateMember = function(userId, event) {
let member = this.members[userId];
if (!member) {
member = new RoomMember(this.roomId, userId);
// add member to members before emitting any events,
// as event handlers often lookup the member
this.members[userId] = member;
this.emit("RoomState.newMember", event, this, member);
}
return member;
};
RoomState.prototype._setStateEvent = function(event) {
if (this.events[event.getType()] === undefined) {
this.events[event.getType()] = {};
}
this.events[event.getType()][event.getStateKey()] = event;
};
RoomState.prototype._updateMember = function(member) {
// this member may have a power level already, so set it.
const pwrLvlEvent = this.getStateEvents("m.room.power_levels", "");
if (pwrLvlEvent) {
member.setPowerLevelEvent(pwrLvlEvent);
}
// blow away the sentinel which is now outdated
delete this._sentinels[member.userId];
this.members[member.userId] = member;
this._joinedMemberCount = null;
this._invitedMemberCount = null;
};
/**
* Get the out-of-band members loading state, whether loading is needed or not.
* Note that loading might be in progress and hence isn't needed.
* @return {bool} whether or not the members of this room need to be loaded
*/
RoomState.prototype.needsOutOfBandMembers = function() {
return this._oobMemberFlags.status === OOB_STATUS_NOTSTARTED;
};
/**
* Mark this room state as waiting for out-of-band members,
* ensuring it doesn't ask for them to be requested again
* through needsOutOfBandMembers
*/
RoomState.prototype.markOutOfBandMembersStarted = function() {
if (this._oobMemberFlags.status !== OOB_STATUS_NOTSTARTED) {
return;
}
this._oobMemberFlags.status = OOB_STATUS_INPROGRESS;
};
/**
* Mark this room state as having failed to fetch out-of-band members
*/
RoomState.prototype.markOutOfBandMembersFailed = function() {
if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
return;
}
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
};
/**
* Clears the loaded out-of-band members
*/
RoomState.prototype.clearOutOfBandMembers = function() {
let count = 0;
Object.keys(this.members).forEach((userId) => {
const member = this.members[userId];
if (member.isOutOfBand()) {
++count;
delete this.members[userId];
}
});
console.log(`LL: RoomState removed ${count} members...`);
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
};
/**
* Sets the loaded out-of-band members.
* @param {MatrixEvent[]} stateEvents array of membership state events
*/
RoomState.prototype.setOutOfBandMembers = function(stateEvents) {
console.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
return;
}
console.log(`LL: RoomState put in OOB_STATUS_FINISHED state ...`);
this._oobMemberFlags.status = OOB_STATUS_FINISHED;
stateEvents.forEach((e) => this._setOutOfBandMember(e));
};
/**
* Sets a single out of band member, used by both setOutOfBandMembers and clone
* @param {MatrixEvent} stateEvent membership state event
*/
RoomState.prototype._setOutOfBandMember = function(stateEvent) {
if (stateEvent.getType() !== 'm.room.member') {
return;
}
const userId = stateEvent.getStateKey();
const existingMember = this.getMember(userId);
// never replace members received as part of the sync
if (existingMember && !existingMember.isOutOfBand()) {
return;
}
const member = this._getOrCreateMember(userId, stateEvent);
member.setMembershipEvent(stateEvent, this);
// needed to know which members need to be stored seperately
// as they are not part of the sync accumulator
// this is cleared by setMembershipEvent so when it's updated through /sync
member.markOutOfBand();
_updateDisplayNameCache(this, member.userId, member.name);
this._setStateEvent(stateEvent);
this._updateMember(member);
this.emit("RoomState.members", stateEvent, this, member);
};
/**
* Set the current typing event for this room.
* @param {MatrixEvent} event The typing event
@@ -246,6 +527,49 @@ RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
return this._displayNameToUserIds[displayName] || [];
};
/**
* Returns true if userId is in room, event is not redacted and either sender of
* mxEvent or has power level sufficient to redact events other than their own.
* @param {MatrixEvent} mxEvent The event to test permission for
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given used ID can redact given event
*/
RoomState.prototype.maySendRedactionForEvent = function(mxEvent, userId) {
const member = this.getMember(userId);
if (!member || member.membership === 'leave') return false;
if (mxEvent.status || mxEvent.isRedacted()) return false;
// The user may have been the sender, but they can't redact their own message
// if redactions are blocked.
const canRedact = this.maySendEvent("m.room.redaction", userId);
if (mxEvent.getSender() === userId) return canRedact;
return this._hasSufficientPowerLevelFor('redact', member.powerLevel);
};
/**
* Returns true if the given power level is sufficient for action
* @param {string} action The type of power level to check
* @param {number} powerLevel The power level of the member
* @return {boolean} true if the given power level is sufficient
*/
RoomState.prototype._hasSufficientPowerLevelFor = function(action, powerLevel) {
const powerLevelsEvent = this.getStateEvents('m.room.power_levels', '');
let powerLevels = {};
if (powerLevelsEvent) {
powerLevels = powerLevelsEvent.getContent();
}
let requiredLevel = 50;
if (utils.isNumber(powerLevels[action])) {
requiredLevel = powerLevels[action];
}
return powerLevel >= requiredLevel;
};
/**
* Short-form for maySendEvent('m.room.message', userId)
* @param {string} userId The user ID of the user to test permission for
@@ -269,7 +593,6 @@ RoomState.prototype.maySendEvent = function(eventType, userId) {
return this._maySendEventOfType(eventType, userId, false);
};
/**
* Returns true if the given MatrixClient has permission to send a state
* event of type `stateEventType` into this room.
@@ -312,11 +635,6 @@ RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
* according to the room's state.
*/
RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
const member = this.getMember(userId);
if (!member || member.membership == 'leave') {
return false;
}
const power_levels_event = this.getStateEvents('m.room.power_levels', '');
let power_levels;
@@ -324,25 +642,63 @@ RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
let state_default = 0;
let events_default = 0;
let powerLevel = 0;
if (power_levels_event) {
power_levels = power_levels_event.getContent();
events_levels = power_levels.events || {};
if (power_levels.state_default !== undefined) {
if (Number.isFinite(power_levels.state_default)) {
state_default = power_levels.state_default;
} else {
state_default = 50;
}
if (power_levels.events_default !== undefined) {
const userPowerLevel = power_levels.users && power_levels.users[userId];
if (Number.isFinite(userPowerLevel)) {
powerLevel = userPowerLevel;
} else if(Number.isFinite(power_levels.users_default)) {
powerLevel = power_levels.users_default;
}
if (Number.isFinite(power_levels.events_default)) {
events_default = power_levels.events_default;
}
}
let required_level = state ? state_default : events_default;
if (events_levels[eventType] !== undefined) {
if (Number.isFinite(events_levels[eventType])) {
required_level = events_levels[eventType];
}
return member.powerLevel >= required_level;
return powerLevel >= required_level;
};
/**
* Returns true if the given user ID has permission to trigger notification
* of type `notifLevelKey`
* @param {string} notifLevelKey The level of notification to test (eg. 'room')
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID has permission to trigger a
* notification of this type.
*/
RoomState.prototype.mayTriggerNotifOfType = function(notifLevelKey, userId) {
const member = this.getMember(userId);
if (!member) {
return false;
}
const powerLevelsEvent = this.getStateEvents('m.room.power_levels', '');
let notifLevel = 50;
if (
powerLevelsEvent &&
powerLevelsEvent.getContent() &&
powerLevelsEvent.getContent().notifications &&
utils.isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey])
) {
notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey];
}
return member.powerLevel >= notifLevel;
};
/**
@@ -376,22 +732,26 @@ function _updateDisplayNameCache(roomState, userId, displayName) {
// We clobber the user_id > name lookup but the name -> [user_id] lookup
// means we need to remove that user ID from that array rather than nuking
// the lot.
const existingUserIds = roomState._displayNameToUserIds[oldName] || [];
for (let i = 0; i < existingUserIds.length; i++) {
if (existingUserIds[i] === userId) {
// remove this user ID from this array
existingUserIds.splice(i, 1);
i--;
}
const strippedOldName = utils.removeHiddenChars(oldName);
const existingUserIds = roomState._displayNameToUserIds[strippedOldName];
if (existingUserIds) {
// remove this user ID from this array
const filteredUserIDs = existingUserIds.filter((id) => id !== userId);
roomState._displayNameToUserIds[strippedOldName] = filteredUserIDs;
}
roomState._displayNameToUserIds[oldName] = existingUserIds;
}
roomState._userIdsToDisplayNames[userId] = displayName;
if (!roomState._displayNameToUserIds[displayName]) {
roomState._displayNameToUserIds[displayName] = [];
const strippedDisplayname = displayName && utils.removeHiddenChars(displayName);
// an empty stripped displayname (undefined/'') will be set to MXID in room-member.js
if (strippedDisplayname) {
if (!roomState._displayNameToUserIds[strippedDisplayname]) {
roomState._displayNameToUserIds[strippedDisplayname] = [];
}
roomState._displayNameToUserIds[strippedDisplayname].push(userId);
}
roomState._displayNameToUserIds[displayName].push(userId);
}
/**
@@ -421,7 +781,8 @@ function _updateDisplayNameCache(roomState, userId, displayName) {
/**
* Fires whenever a member is added to the members dictionary. The RoomMember
* will not be fully populated yet (e.g. no membership state).
* will not be fully populated yet (e.g. no membership state) but will already
* be available in the members dictionary.
* @event module:client~MatrixClient#"RoomState.newMember"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.members dictionary
+462 -108
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 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.
@@ -21,12 +22,16 @@ const EventEmitter = require("events").EventEmitter;
const EventStatus = require("./event").EventStatus;
const RoomSummary = require("./room-summary");
const RoomMember = require("./room-member");
const MatrixEvent = require("./event").MatrixEvent;
const utils = require("../utils");
const ContentRepo = require("../content-repo");
const EventTimeline = require("./event-timeline");
const EventTimelineSet = require("./event-timeline-set");
import ReEmitter from '../ReEmitter';
const LATEST_ROOM_VERSION = '1';
function synthesizeReceipt(userId, event, receiptType) {
// console.log("synthesizing receipt for "+event.getId());
@@ -67,6 +72,8 @@ function synthesizeReceipt(userId, event, receiptType) {
* @constructor
* @alias module:models/room
* @param {string} roomId Required. The ID of this room.
* @param {MatrixClient} client Required. The client, used to lazy load members.
* @param {string} myUserId Required. The ID of the syncing user.
* @param {Object=} opts Configuration options
* @param {*} opts.storageToken Optional. The token which a data store can use
* to remember the state of the room. What this means is dependent on the store
@@ -93,18 +100,20 @@ function synthesizeReceipt(userId, event, receiptType) {
* event type and the values are the events.
* @prop {RoomState} oldState The state of the room at the time of the oldest
* event in the live timeline. Present for backwards compatibility -
* prefer getLiveTimeline().getState(true).
* prefer getLiveTimeline().getState(EventTimeline.BACKWARDS).
* @prop {RoomState} currentState The state of the room at the time of the
* newest event in the timeline. Present for backwards compatibility -
* prefer getLiveTimeline().getState(false).
* prefer getLiveTimeline().getState(EventTimeline.FORWARDS).
* @prop {RoomSummary} summary The room summary.
* @prop {*} storageToken A token which a data store can use to remember
* the state of the room.
*/
function Room(roomId, opts) {
function Room(roomId, client, myUserId, opts) {
opts = opts || {};
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
this.reEmitter = new ReEmitter(this);
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
throw new Error(
"opts.pendingEventOrdering MUST be either 'chronological' or " +
@@ -112,6 +121,7 @@ function Room(roomId, opts) {
);
}
this.myUserId = myUserId;
this.roomId = roomId;
this.name = roomId;
this.tags = {
@@ -152,7 +162,7 @@ function Room(roomId, opts) {
// all our per-room timeline sets. the first one is the unfiltered ones;
// the subsequent ones are the filtered ones in no particular order.
this._timelineSets = [new EventTimelineSet(this, opts)];
reEmit(this, this.getUnfilteredTimelineSet(),
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(),
["Room.timeline", "Room.timelineReset"]);
this._fixUpLegacyTimelineFields();
@@ -166,10 +176,58 @@ function Room(roomId, opts) {
this._pendingEventList = [];
}
this._blacklistUnverifiedDevices = false; // read by megolm
// read by megolm; boolean value - null indicates "use global value"
this._blacklistUnverifiedDevices = null;
this._selfMembership = null;
this._summaryHeroes = null;
// awaited by getEncryptionTargetMembers while room members are loading
this._client = client;
if (!this._opts.lazyLoadMembers) {
this._membersPromise = Promise.resolve();
} else {
this._membersPromise = null;
}
}
utils.inherits(Room, EventEmitter);
/**
* Gets the version of the room
* @returns {string} The version of the room, or null if it could not be determined
*/
Room.prototype.getVersion = function() {
const createEvent = this.currentState.getStateEvents("m.room.create", "");
if (!createEvent) {
console.warn("Room " + this.room_id + " does not have an m.room.create event");
return '1';
}
const ver = createEvent.getContent()['room_version'];
if (ver === undefined) return '1';
return ver;
};
/**
* Determines whether this room needs to be upgraded to a new version
* @returns {string?} What version the room should be upgraded to, or null if
* the room does not require upgrading at this time.
*/
Room.prototype.shouldUpgradeToVersion = function() {
// This almost certainly won't be the way this actually works - this
// is essentially a stub method.
if (this.getVersion() === LATEST_ROOM_VERSION) return null;
return LATEST_ROOM_VERSION;
};
/**
* Determines whether the given user is permitted to perform a room upgrade
* @param {String} userId The ID of the user to test against
* @returns {bool} True if the given user is permitted to upgrade the room
*/
Room.prototype.userMayUpgradeRoom = function(userId) {
return this.currentState.maySendStateEvent("m.room.tombstone", userId);
};
/**
* Get the list of pending sent events for this room
*
@@ -197,6 +255,232 @@ Room.prototype.getLiveTimeline = function() {
return this.getUnfilteredTimelineSet().getLiveTimeline();
};
/**
* @param {string} myUserId the user id for the logged in member
* @return {string} the membership type (join | leave | invite) for the logged in user
*/
Room.prototype.getMyMembership = function() {
return this._selfMembership;
};
/**
* If this room is a DM we're invited to,
* try to find out who invited us
* @return {string} user id of the inviter
*/
Room.prototype.getDMInviter = function() {
if (this.myUserId) {
const me = this.getMember(this.myUserId);
if (me) {
return me.getDMInviter();
}
}
if (this._selfMembership === "invite") {
// fall back to summary information
const memberCount = this.getInvitedAndJoinedMemberCount();
if (memberCount == 2 && this._summaryHeroes.length) {
return this._summaryHeroes[0];
}
}
};
/**
* Assuming this room is a DM room, tries to guess with which user.
* @return {string} user id of the other member (could be syncing user)
*/
Room.prototype.guessDMUserId = function() {
const me = this.getMember(this.myUserId);
if (me) {
const inviterId = me.getDMInviter();
if (inviterId) {
return inviterId;
}
}
// remember, we're assuming this room is a DM,
// so returning the first member we find should be fine
const hasHeroes = Array.isArray(this._summaryHeroes) &&
this._summaryHeroes.length;
if (hasHeroes) {
return this._summaryHeroes[0];
}
const members = this.currentState.getMembers();
const anyMember = members.find((m) => m.userId !== this.myUserId);
if (anyMember) {
return anyMember.userId;
}
// it really seems like I'm the only user in the room
// so I probably created a room with just me in it
// and marked it as a DM. Ok then
return this.myUserId;
};
Room.prototype.getAvatarFallbackMember = function() {
const memberCount = this.getInvitedAndJoinedMemberCount();
if (memberCount > 2) {
return;
}
const hasHeroes = Array.isArray(this._summaryHeroes) &&
this._summaryHeroes.length;
if (hasHeroes) {
const availableMember = this._summaryHeroes.map((userId) => {
return this.getMember(userId);
}).find((member) => !!member);
if (availableMember) {
return availableMember;
}
}
const members = this.currentState.getMembers();
// could be different than memberCount
// as this includes left members
if (members.length <= 2) {
const availableMember = members.find((m) => {
return m.userId !== this.myUserId;
});
if (availableMember) {
return availableMember;
}
}
// if all else fails, try falling back to a user,
// and create a one-off member for it
if (hasHeroes) {
const availableUser = this._summaryHeroes.map((userId) => {
return this._client.getUser(userId);
}).find((user) => !!user);
if (availableUser) {
const member = new RoomMember(
this.roomId, availableUser.userId);
member.user = availableUser;
return member;
}
}
};
/**
* Sets the membership this room was received as during sync
* @param {string} membership join | leave | invite
*/
Room.prototype.updateMyMembership = function(membership) {
const prevMembership = this._selfMembership;
this._selfMembership = membership;
if (prevMembership !== membership) {
if (membership === "leave") {
this._cleanupAfterLeaving();
}
this.emit("Room.myMembership", this, membership, prevMembership);
}
};
Room.prototype._loadMembersFromServer = async function() {
const lastSyncToken = this._client.store.getSyncToken();
const queryString = utils.encodeParams({
not_membership: "leave",
at: lastSyncToken,
});
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString,
{$roomId: this.roomId});
const http = this._client._http;
const response = await http.authedRequest(undefined, "GET", path);
return response.chunk;
};
Room.prototype._loadMembers = async function() {
// were the members loaded from the server?
let fromServer = false;
let rawMembersEvents =
await this._client.store.getOutOfBandMembers(this.roomId);
if (rawMembersEvents === null) {
fromServer = true;
rawMembersEvents = await this._loadMembersFromServer();
console.log(`LL: got ${rawMembersEvents.length} ` +
`members from server for room ${this.roomId}`);
}
const memberEvents = rawMembersEvents.map(this._client.getEventMapper());
return {memberEvents, fromServer};
};
/**
* Preloads the member list in case lazy loading
* of memberships is in use. Can be called multiple times,
* it will only preload once.
* @return {Promise} when preloading is done and
* accessing the members on the room will take
* all members in the room into account
*/
Room.prototype.loadMembersIfNeeded = function() {
if (this._membersPromise) {
return this._membersPromise;
}
// mark the state so that incoming messages while
// the request is in flight get marked as superseding
// the OOB members
this.currentState.markOutOfBandMembersStarted();
const inMemoryUpdate = this._loadMembers().then((result) => {
this.currentState.setOutOfBandMembers(result.memberEvents);
// now the members are loaded, start to track the e2e devices if needed
if (this._client.isRoomEncrypted(this.roomId)) {
this._client._crypto.trackRoomDevices(this.roomId);
}
return result.fromServer;
}).catch((err) => {
// allow retries on fail
this._membersPromise = null;
this.currentState.markOutOfBandMembersFailed();
throw err;
});
// update members in storage, but don't wait for it
inMemoryUpdate.then((fromServer) => {
if (fromServer) {
const oobMembers = this.currentState.getMembers()
.filter((m) => m.isOutOfBand())
.map((m) => m.events.member.event);
console.log(`LL: telling store to write ${oobMembers.length}`
+ ` members for room ${this.roomId}`);
const store = this._client.store;
return store.setOutOfBandMembers(this.roomId, oobMembers)
// swallow any IDB error as we don't want to fail
// because of this
.catch((err) => {
console.log("LL: storing OOB room members failed, oh well",
err);
});
}
}).catch((err) => {
// as this is not awaited anywhere,
// at least show the error in the console
console.error(err);
});
this._membersPromise = inMemoryUpdate;
return this._membersPromise;
};
/**
* Removes the lazily loaded members from storage if needed
*/
Room.prototype.clearLoadedMembersIfNeeded = async function() {
if (this._opts.lazyLoadMembers && this._membersPromise) {
await this.loadMembersIfNeeded();
await this._client.store.clearOutOfBandMembers(this.roomId);
this.currentState.clearOutOfBandMembers();
this._membersPromise = null;
}
};
/**
* called when sync receives this room in the leave section
* to do cleanup after leaving a room. Possibly called multiple times.
*/
Room.prototype._cleanupAfterLeaving = function() {
this.clearLoadedMembersIfNeeded().catch((err) => {
console.error(`error after clearing loaded members from ` +
`room ${this.roomId} after leaving`);
console.dir(err);
});
};
/**
* Reset the live timeline of all timelineSets, and start new ones.
@@ -204,10 +488,16 @@ Room.prototype.getLiveTimeline = function() {
* <p>This is used when /sync returns a 'limited' timeline.
*
* @param {string=} backPaginationToken token for back-paginating the new timeline
* @param {string=} forwardPaginationToken token for forward-paginating the old live timeline,
* if absent or null, all timelines are reset, removing old ones (including the previous live
* timeline which would otherwise be unable to paginate forwards without this token).
* Removing just the old live timeline whilst preserving previous ones is not supported.
*/
Room.prototype.resetLiveTimeline = function(backPaginationToken) {
Room.prototype.resetLiveTimeline = function(backPaginationToken, forwardPaginationToken) {
for (let i = 0; i < this._timelineSets.length; i++) {
this._timelineSets[i].resetLiveTimeline(backPaginationToken);
this._timelineSets[i].resetLiveTimeline(
backPaginationToken, forwardPaginationToken,
);
}
this._fixUpLegacyTimelineFields();
@@ -296,10 +586,30 @@ Room.prototype.setUnreadNotificationCount = function(type, count) {
this._notificationCounts[type] = count;
};
Room.prototype.setSummary = function(summary) {
const heroes = summary["m.heroes"];
const joinedCount = summary["m.joined_member_count"];
const invitedCount = summary["m.invited_member_count"];
if (Number.isInteger(joinedCount)) {
this.currentState.setJoinedMemberCount(joinedCount);
}
if (Number.isInteger(invitedCount)) {
this.currentState.setInvitedMemberCount(invitedCount);
}
if (Array.isArray(heroes)) {
// be cautious about trusting server values,
// and make sure heroes doesn't contain our own id
// just to be sure
this._summaryHeroes = heroes.filter((userId) => {
return userId !== this.myUserId;
});
}
};
/**
* Whether to send encrypted messages to devices within this room.
* Will be ignored if MatrixClient's blacklistUnverifiedDevices setting is true.
* @param {boolean} value if true, blacklist unverified devices.
* @param {Boolean} value true to blacklist unverified devices, null
* to use the global value for this room.
*/
Room.prototype.setBlacklistUnverifiedDevices = function(value) {
this._blacklistUnverifiedDevices = value;
@@ -307,8 +617,8 @@ Room.prototype.setBlacklistUnverifiedDevices = function(value) {
/**
* Whether to send encrypted messages to devices within this room.
* Will be ignored if MatrixClient's blacklistUnverifiedDevices setting is true.
* @return {boolean} true if blacklisting unverified devices.
* @return {Boolean} true if blacklisting unverified devices, null
* if the global value should be used for this room.
*/
Room.prototype.getBlacklistUnverifiedDevices = function() {
return this._blacklistUnverifiedDevices;
@@ -420,11 +730,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
* @return {RoomMember} The member or <code>null</code>.
*/
Room.prototype.getMember = function(userId) {
const member = this.currentState.members[userId];
if (!member) {
return null;
}
return member;
return this.currentState.getMember(userId);
};
/**
@@ -435,6 +741,33 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
return this.getMembersWithMembership("join");
};
/**
* Returns the number of joined members in this room
* This method caches the result.
* This is a wrapper around the method of the same name in roomState, returning
* its result for the room's current state.
* @return {integer} The number of members in this room whose membership is 'join'
*/
Room.prototype.getJoinedMemberCount = function() {
return this.currentState.getJoinedMemberCount();
};
/**
* Returns the number of invited members in this room
* @return {integer} The number of members in this room whose membership is 'invite'
*/
Room.prototype.getInvitedMemberCount = function() {
return this.currentState.getInvitedMemberCount();
};
/**
* Returns the number of invited + joined members in this room
* @return {integer} The number of members in this room whose membership is 'invite' or 'join'
*/
Room.prototype.getInvitedAndJoinedMemberCount = function() {
return this.getInvitedMemberCount() + this.getJoinedMemberCount();
};
/**
* Get a list of members with given membership state.
* @param {string} membership The membership state.
@@ -446,6 +779,29 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
});
};
/**
* Get a list of members we should be encrypting for in this room
* @return {Promise<RoomMember[]>} A list of members who
* we should encrypt messages for in this room.
*/
Room.prototype.getEncryptionTargetMembers = async function() {
await this.loadMembersIfNeeded();
let members = this.getMembersWithMembership("join");
if (this.shouldEncryptForInvitedMembers()) {
members = members.concat(this.getMembersWithMembership("invite"));
}
return members;
};
/**
* Determine whether we should encrypt messages for invited users in this room
* @return {boolean} if we should encrypt messages for invited users
*/
Room.prototype.shouldEncryptForInvitedMembers = function() {
const ev = this.currentState.getStateEvents("m.room.history_visibility", "");
return (ev && ev.getContent() && ev.getContent().history_visibility !== "joined");
};
/**
* Get the default room name (i.e. what a given user would see if the
* room had no m.room.name)
@@ -483,7 +839,7 @@ Room.prototype.getOrCreateFilteredTimelineSet = function(filter) {
}
const opts = Object.assign({ filter: filter }, this._opts);
const timelineSet = new EventTimelineSet(this, opts);
reEmit(this, timelineSet, ["Room.timeline", "Room.timelineReset"]);
this.reEmitter.reEmit(timelineSet, ["Room.timeline", "Room.timelineReset"]);
this._filteredTimelineSets[filter.filterId] = timelineSet;
this._timelineSets.push(timelineSet);
@@ -587,7 +943,8 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
// synthesize and inject implicit read receipts
// Done after adding the event because otherwise the app would get a read receipt
// pointing to an event that wasn't yet in the timeline
if (event.sender) {
// Don't synthesize RR for m.room.redaction as this causes the RR to go missing.
if (event.sender && event.getType() !== "m.room.redaction") {
this.addReceipt(synthesizeReceipt(
event.sender.userId, event, "m.read",
), true);
@@ -749,6 +1106,8 @@ ALLOWED_TRANSITIONS[EventStatus.CANCELLED] =
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
*/
Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
console.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()}`);
// if the message was sent, we expect an event id
if (newStatus == EventStatus.SENT && !newEventId) {
throw new Error("updatePendingEvent called with status=SENT, " +
@@ -896,15 +1255,14 @@ Room.prototype.removeEvent = function(eventId) {
* Recalculate various aspects of the room, including the room name and
* room summary. Call this any time the room's current state is modified.
* May fire "Room.name" if the room name is updated.
* @param {string} userId The client's user ID.
* @fires module:client~MatrixClient#event:"Room.name"
*/
Room.prototype.recalculate = function(userId) {
Room.prototype.recalculate = function() {
// set fake stripped state events if this is an invite room so logic remains
// consistent elsewhere.
const self = this;
const membershipEvent = this.currentState.getStateEvents(
"m.room.member", userId,
"m.room.member", this.myUserId,
);
if (membershipEvent && membershipEvent.getContent().membership === "invite") {
const strippedStateEvents = membershipEvent.event.invite_room_state || [];
@@ -920,14 +1278,14 @@ Room.prototype.recalculate = function(userId) {
content: strippedEvent.content,
event_id: "$fake" + Date.now(),
room_id: self.roomId,
user_id: userId, // technically a lie
user_id: self.myUserId, // technically a lie
})]);
}
});
}
const oldName = this.name;
this.name = calculateRoomName(this, userId);
this.name = calculateRoomName(this, this.myUserId);
this.summary = new RoomSummary(this.roomId, {
title: this.name,
});
@@ -937,7 +1295,6 @@ Room.prototype.recalculate = function(userId) {
}
};
/**
* Get a list of user IDs who have <b>read up to</b> the given event.
* @param {MatrixEvent} event the event to get read receipts for.
@@ -1109,7 +1466,7 @@ Room.prototype.addTags = function(event) {
// }
// XXX: do we need to deep copy here?
this.tags = event.getContent().tags;
this.tags = event.getContent().tags || {};
// XXX: we could do a deep-comparison to see if the tags have really
// changed - but do we want to bother?
@@ -1140,6 +1497,17 @@ Room.prototype.getAccountData = function(type) {
return this.accountData[type];
};
/**
* Returns wheter the syncing user has permission to send a message in the room
* @return {boolean} true if the user should be permitted to send
* message events into the room.
*/
Room.prototype.maySendMessage = function() {
return this.getMyMembership() === 'join' &&
this.currentState.maySendEvent('m.room.message', this.myUserId);
};
/**
* This is an internal method. Calculates the name of the room from the current
* room state.
@@ -1173,98 +1541,84 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
return alias;
}
// get members that are NOT ourselves and are actually in the room.
const otherMembers = utils.filter(room.currentState.getMembers(), function(m) {
return (m.userId !== userId && m.membership !== "leave");
});
const allMembers = utils.filter(room.currentState.getMembers(), function(m) {
return (m.membership !== "leave");
});
const myMemberEventArray = utils.filter(room.currentState.getMembers(), function(m) {
return (m.userId == userId);
});
const myMemberEvent = (
(myMemberEventArray.length && myMemberEventArray[0].events) ?
myMemberEventArray[0].events.member.event : undefined
);
const joinedMemberCount = room.currentState.getJoinedMemberCount();
const invitedMemberCount = room.currentState.getInvitedMemberCount();
// -1 because these numbers include the syncing user
const inviteJoinCount = joinedMemberCount + invitedMemberCount - 1;
// TODO: Localisation
if (myMemberEvent && myMemberEvent.content.membership == "invite") {
if (room.currentState.getMember(myMemberEvent.sender)) {
// extract who invited us to the room
return "Invite from " + room.currentState.getMember(
myMemberEvent.sender,
).name;
} else if (allMembers[0].events.member) {
// use the sender field from the invite event, although this only
// gets us the mxid
return "Invite from " + myMemberEvent.sender;
} else {
return "Room Invite";
}
// get members that are NOT ourselves and are actually in the room.
let otherNames = null;
if (room._summaryHeroes) {
// if we have a summary, the member state events
// should be in the room state
otherNames = room._summaryHeroes.map((userId) => {
const member = room.getMember(userId);
return member ? member.name : userId;
});
} else {
let otherMembers = room.currentState.getMembers().filter((m) => {
return m.userId !== userId &&
(m.membership === "invite" || m.membership === "join");
});
// make sure members have stable order
otherMembers.sort((a, b) => a.userId.localeCompare(b.userId));
// only 5 first members, immitate _summaryHeroes
otherMembers = otherMembers.slice(0, 5);
otherNames = otherMembers.map((m) => m.name);
}
if (inviteJoinCount) {
return memberNamesToRoomName(otherNames, inviteJoinCount);
}
if (otherMembers.length === 0) {
if (allMembers.length === 1) {
// self-chat, peeked room with 1 participant,
// or inbound invite, or outbound 3PID invite.
if (allMembers[0].userId === userId) {
const thirdPartyInvites =
room.currentState.getStateEvents("m.room.third_party_invite");
if (thirdPartyInvites && thirdPartyInvites.length > 0) {
let name = "Inviting " +
thirdPartyInvites[0].getContent().display_name;
if (thirdPartyInvites.length > 1) {
if (thirdPartyInvites.length == 2) {
name += " and " +
thirdPartyInvites[1].getContent().display_name;
} else {
name += " and " +
thirdPartyInvites.length + " others";
}
}
return name;
} else {
return "Empty room";
}
} else {
return allMembers[0].name;
}
} else {
// there really isn't anyone in this room...
return "Empty room";
const myMembership = room.getMyMembership();
// if I have created a room and invited people throuh
// 3rd party invites
if (myMembership == 'join') {
const thirdPartyInvites =
room.currentState.getStateEvents("m.room.third_party_invite");
if (thirdPartyInvites && thirdPartyInvites.length) {
const thirdPartyNames = thirdPartyInvites.map((i) => {
return i.getContent().display_name;
});
return `Inviting ${memberNamesToRoomName(thirdPartyNames)}`;
}
} else if (otherMembers.length === 1) {
return otherMembers[0].name;
} else if (otherMembers.length === 2) {
return (
otherMembers[0].name + " and " + otherMembers[1].name
);
}
// let's try to figure out who was here before
let leftNames = otherNames;
// if we didn't have heroes, try finding them in the room state
if(!leftNames.length) {
leftNames = room.currentState.getMembers().filter((m) => {
return m.userId !== userId &&
m.membership !== "invite" &&
m.membership !== "join";
}).map((m) => m.name);
}
if(leftNames.length) {
return `Empty room (was ${memberNamesToRoomName(leftNames)})`;
} else {
return (
otherMembers[0].name + " and " + (otherMembers.length - 1) + " others"
);
return "Empty room";
}
}
// FIXME: copypasted from sync.js
function reEmit(reEmitEntity, emittableEntity, eventNames) {
utils.forEach(eventNames, function(eventName) {
// setup a listener on the entity (the Room, User, etc) for this event
emittableEntity.on(eventName, function() {
// take the args from the listener and reuse them, adding the
// event name to the arg list so it works with .emit()
// Transformation Example:
// listener on "foo" => function(a,b) { ... }
// Re-emit on "thing" => thing.emit("foo", a, b)
const newArgs = [eventName];
for (let i = 0; i < arguments.length; i++) {
newArgs.push(arguments[i]);
}
reEmitEntity.emit(...newArgs);
});
});
function memberNamesToRoomName(names, count = (names.length + 1)) {
const countWithoutMe = count - 1;
if (!names.length) {
return count <= 1 ? "Empty room" : null;
} else if (names.length === 1 && countWithoutMe <= 1) {
return names[0];
} else if (names.length === 2 && countWithoutMe <= 2) {
return `${names[0]} and ${names[1]}`;
} else {
const plural = countWithoutMe > 1;
if (plural) {
return `${names[0]} and ${countWithoutMe} others`;
} else {
return `${names[0]} and 1 other`;
}
}
}
/**
+97 -30
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket 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.
@@ -17,6 +18,8 @@ limitations under the License.
* @module pushprocessor
*/
const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
/**
* Construct a Push Processor.
* @constructor
@@ -27,12 +30,15 @@ function PushProcessor(client) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
const matchingRuleFromKindSet = function(ev, kindset, device) {
const rulekinds_in_order = ['override', 'content', 'room', 'sender', 'underride'];
const cachedGlobToRegex = {
// $glob: RegExp,
};
const matchingRuleFromKindSet = (ev, kindset, device) => {
for (let ruleKindIndex = 0;
ruleKindIndex < rulekinds_in_order.length;
ruleKindIndex < RULEKINDS_IN_ORDER.length;
++ruleKindIndex) {
const kind = rulekinds_in_order[ruleKindIndex];
const kind = RULEKINDS_IN_ORDER[ruleKindIndex];
const ruleset = kindset[kind];
for (let ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
@@ -46,7 +52,7 @@ function PushProcessor(client) {
continue;
}
if (ruleMatchesEvent(rawrule, ev)) {
if (this.ruleMatchesEvent(rawrule, ev)) {
rule.kind = kind;
return rule;
}
@@ -73,7 +79,7 @@ function PushProcessor(client) {
rawrule.conditions.push({
'kind': 'event_match',
'key': 'room_id',
'pattern': tprule.rule_id,
'value': tprule.rule_id,
});
break;
case 'sender':
@@ -83,7 +89,7 @@ function PushProcessor(client) {
rawrule.conditions.push({
'kind': 'event_match',
'key': 'user_id',
'pattern': tprule.rule_id,
'value': tprule.rule_id,
});
break;
case 'content':
@@ -106,27 +112,38 @@ function PushProcessor(client) {
return rawrule;
};
const ruleMatchesEvent = function(rule, ev) {
let ret = true;
for (let i = 0; i < rule.conditions.length; ++i) {
const cond = rule.conditions[i];
ret &= eventFulfillsCondition(cond, ev);
}
//console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match"));
return ret;
};
const eventFulfillsCondition = function(cond, ev) {
const condition_functions = {
"event_match": eventFulfillsEventMatchCondition,
"device": eventFulfillsDeviceCondition,
"contains_display_name": eventFulfillsDisplayNameCondition,
"room_member_count": eventFulfillsRoomMemberCountCondition,
"sender_notification_permission": eventFulfillsSenderNotifPermCondition,
};
if (condition_functions[cond.kind]) {
return condition_functions[cond.kind](cond, ev);
}
return true;
// unknown conditions: we previously matched all unknown conditions,
// but given that rules can be added to the base rules on a server,
// it's probably better to not match unknown conditions.
return false;
};
const eventFulfillsSenderNotifPermCondition = function(cond, ev) {
const notifLevelKey = cond['key'];
if (!notifLevelKey) {
return false;
}
const room = client.getRoom(ev.getRoomId());
if (!room || !room.currentState) {
return false;
}
// Note that this should not be the current state of the room but the state at
// the point the event is in the DAG. Unfortunately the js-sdk does not store
// this.
return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender());
};
const eventFulfillsRoomMemberCountCondition = function(cond, ev) {
@@ -139,9 +156,7 @@ function PushProcessor(client) {
return false;
}
const memberCount = Object.keys(room.currentState.members).filter(function(m) {
return room.currentState.members[m].membership == 'join';
}).length;
const memberCount = room.currentState.getJoinedMemberCount();
const m = cond.is.match(/^([=<>]*)([0-9]*)$/);
if (!m) {
@@ -194,30 +209,50 @@ function PushProcessor(client) {
};
const eventFulfillsEventMatchCondition = function(cond, ev) {
if (!cond.key) {
return false;
}
const val = valueForDottedKey(cond.key, ev);
if (!val || typeof val != 'string') {
return false;
}
let pat;
if (cond.key == 'content.body') {
pat = '(^|\\W)' + globToRegexp(cond.pattern) + '(\\W|$)';
} else {
pat = '^' + globToRegexp(cond.pattern) + '$';
if (cond.value) {
return cond.value === val;
}
const regex = new RegExp(pat, 'i');
let regex;
if (cond.key == 'content.body') {
regex = createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)');
} else {
regex = createCachedRegex('^', cond.pattern, '$');
}
return !!val.match(regex);
};
const createCachedRegex = function(prefix, glob, suffix) {
if (cachedGlobToRegex[glob]) {
return cachedGlobToRegex[glob];
}
cachedGlobToRegex[glob] = new RegExp(
prefix + globToRegexp(glob) + suffix,
'i', // Case insensitive
);
return cachedGlobToRegex[glob];
};
const globToRegexp = function(glob) {
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
// Because micromatch is about 130KB with dependencies,
// and minimatch is not much better.
let pat = escapeRegExp(glob);
pat = pat.replace(/\\\*/, '.*');
pat = pat.replace(/\?/, '.');
pat = pat.replace(/\\\[(!|)(.*)\\]/, function(match, p1, p2, offset, string) {
pat = pat.replace(/\\\*/g, '.*');
pat = pat.replace(/\?/g, '.');
pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) {
const first = p1 && '^' || '';
const second = p2.replace(/\\\-/, '-');
return '[' + first + second + ']';
@@ -291,6 +326,17 @@ function PushProcessor(client) {
return actionObj;
};
this.ruleMatchesEvent = function(rule, ev) {
let ret = true;
for (let i = 0; i < rule.conditions.length; ++i) {
const cond = rule.conditions[i];
ret &= eventFulfillsCondition(cond, ev);
}
//console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match"));
return ret;
};
/**
* Get the user's push actions for the given event
*
@@ -301,6 +347,27 @@ function PushProcessor(client) {
this.actionsForEvent = function(ev) {
return pushActionsForEventAndRulesets(ev, client.pushRules);
};
/**
* Get one of the users push rules by its ID
*
* @param {string} ruleId The ID of the rule to search for
* @return {object} The push rule, or null if no such rule was found
*/
this.getPushRuleById = function(ruleId) {
for (const scope of ['device', 'global']) {
if (client.pushRules[scope] === undefined) continue;
for (const kind of RULEKINDS_IN_ORDER) {
if (client.pushRules[scope][kind] === undefined) continue;
for (const rule of client.pushRules[scope][kind]) {
if (rule.rule_id === ruleId) return rule;
}
}
}
return null;
};
}
/**
+1 -1
View File
@@ -168,7 +168,7 @@ function _runCallbacks() {
for (let i = 0; i < callbacksToRun.length; i++) {
cb = callbacksToRun[i];
try {
cb.func.apply(null, cb.params);
cb.func.apply(global, cb.params);
} catch (e) {
console.error("Uncaught exception in callback function",
e.stack || e);
+4 -2
View File
@@ -20,7 +20,7 @@ limitations under the License.
* @module scheduler
*/
const utils = require("./utils");
const q = require("q");
import Promise from 'bluebird';
const DEBUG = false; // set true to enable console logging.
@@ -83,6 +83,8 @@ MatrixScheduler.prototype.removeEventFromQueue = function(event) {
let removed = false;
utils.removeElement(this._queues[name], function(element) {
if (element.event.getId() === event.getId()) {
// XXX we should probably reject the promise?
// https://github.com/matrix-org/matrix-js-sdk/issues/496
removed = true;
return true;
}
@@ -118,7 +120,7 @@ MatrixScheduler.prototype.queueEvent = function(event) {
if (!this._queues[queueName]) {
this._queues[queueName] = [];
}
const defer = q.defer();
const defer = Promise.defer();
this._queues[queueName].push({
event: event,
defer: defer,
+571
View File
@@ -0,0 +1,571 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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 Promise from 'bluebird';
import SyncAccumulator from "../sync-accumulator";
import utils from "../utils";
const VERSION = 3;
function createDatabase(db) {
// Make user store, clobber based on user ID. (userId property of User objects)
db.createObjectStore("users", { keyPath: ["userId"] });
// Make account data store, clobber based on event type.
// (event.type property of MatrixEvent objects)
db.createObjectStore("accountData", { keyPath: ["type"] });
// Make /sync store (sync tokens, room data, etc), always clobber (const key).
db.createObjectStore("sync", { keyPath: ["clobber"] });
}
function upgradeSchemaV2(db) {
const oobMembersStore = db.createObjectStore(
"oob_membership_events", {
keyPath: ["room_id", "state_key"],
});
oobMembersStore.createIndex("room", "room_id");
}
function upgradeSchemaV3(db) {
db.createObjectStore("client_options",
{ keyPath: ["clobber"]});
}
/**
* Helper method to collect results from a Cursor and promiseify it.
* @param {ObjectStore|Index} store The store to perform openCursor on.
* @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor.
* @param {Function} resultMapper A function which is repeatedly called with a
* Cursor.
* Return the data you want to keep.
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
* resultMapper.
*/
function selectQuery(store, keyRange, resultMapper) {
const query = store.openCursor(keyRange);
return new Promise((resolve, reject) => {
const results = [];
query.onerror = (event) => {
reject(new Error("Query failed: " + event.target.errorCode));
};
// collect results
query.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
resolve(results);
return; // end of results
}
results.push(resultMapper(cursor));
cursor.continue();
};
});
}
function txnAsPromise(txn) {
return new Promise((resolve, reject) => {
txn.oncomplete = function(event) {
resolve(event);
};
txn.onerror = function(event) {
reject(event.target.error);
};
});
}
function reqAsEventPromise(req) {
return new Promise((resolve, reject) => {
req.onsuccess = function(event) {
resolve(event);
};
req.onerror = function(event) {
reject(event.target.error);
};
});
}
function reqAsPromise(req) {
return new Promise((resolve, reject) => {
req.onsuccess = () => resolve(req);
req.onerror = (err) => reject(err);
});
}
function reqAsCursorPromise(req) {
return reqAsEventPromise(req).then((event) => event.target.result);
}
/**
* Does the actual reading from and writing to the indexeddb
*
* Construct a new Indexed Database store backend. This requires a call to
* <code>connect()</code> before this store can be used.
* @constructor
* @param {Object} indexedDBInterface The Indexed DB interface e.g
* <code>window.indexedDB</code>
* @param {string=} dbName Optional database name. The same name must be used
* to open the same database.
*/
const LocalIndexedDBStoreBackend = function LocalIndexedDBStoreBackend(
indexedDBInterface, dbName,
) {
this.indexedDB = indexedDBInterface;
this._dbName = "matrix-js-sdk:" + (dbName || "default");
this.db = null;
this._disconnected = true;
this._syncAccumulator = new SyncAccumulator();
this._isNewlyCreated = false;
};
LocalIndexedDBStoreBackend.prototype = {
/**
* Attempt to connect to the database. This can fail if the user does not
* grant permission.
* @return {Promise} Resolves if successfully connected.
*/
connect: function() {
if (!this._disconnected) {
console.log(
`LocalIndexedDBStoreBackend.connect: already connected or connecting`,
);
return Promise.resolve();
}
this._disconnected = false;
console.log(
`LocalIndexedDBStoreBackend.connect: connecting...`,
);
const req = this.indexedDB.open(this._dbName, VERSION);
req.onupgradeneeded = (ev) => {
const db = ev.target.result;
const oldVersion = ev.oldVersion;
console.log(
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
);
if (oldVersion < 1) { // The database did not previously exist.
this._isNewlyCreated = true;
createDatabase(db);
}
if (oldVersion < 2) {
upgradeSchemaV2(db);
}
if (oldVersion < 3) {
upgradeSchemaV3(db);
}
// Expand as needed.
};
req.onblocked = () => {
console.log(
`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`,
);
};
console.log(
`LocalIndexedDBStoreBackend.connect: awaiting connection...`,
);
return reqAsEventPromise(req).then((ev) => {
console.log(
`LocalIndexedDBStoreBackend.connect: connected`,
);
this.db = ev.target.result;
// add a poorly-named listener for when deleteDatabase is called
// so we can close our db connections.
this.db.onversionchange = () => {
this.db.close();
};
return this._init();
});
},
/** @return {bool} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return Promise.resolve(this._isNewlyCreated);
},
/**
* Having connected, load initial data from the database and prepare for use
* @return {Promise} Resolves on success
*/
_init: function() {
return Promise.all([
this._loadAccountData(),
this._loadSyncData(),
]).then(([accountData, syncData]) => {
console.log(
`LocalIndexedDBStoreBackend: loaded initial data`,
);
this._syncAccumulator.accumulate({
next_batch: syncData.nextBatch,
rooms: syncData.roomsData,
groups: syncData.groupsData,
account_data: {
events: accountData,
},
});
});
},
/**
* Returns the out-of-band membership events for this room that
* were previously loaded.
* @param {string} roomId
* @returns {Promise<event[]>} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
getOutOfBandMembers: function(roomId) {
return new Promise((resolve, reject) =>{
const tx = this.db.transaction(["oob_membership_events"], "readonly");
const store = tx.objectStore("oob_membership_events");
const roomIndex = store.index("room");
const range = IDBKeyRange.only(roomId);
const request = roomIndex.openCursor(range);
const membershipEvents = [];
// did we encounter the oob_written marker object
// amongst the results? That means OOB member
// loading already happened for this room
// but there were no members to persist as they
// were all known already
let oobWritten = false;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
// Unknown room
if (!membershipEvents.length && !oobWritten) {
return resolve(null);
}
return resolve(membershipEvents);
}
const record = cursor.value;
if (record.oob_written) {
oobWritten = true;
} else {
membershipEvents.push(record);
}
cursor.continue();
};
request.onerror = (err) => {
reject(err);
};
}).then((events) => {
console.log(`LL: got ${events && events.length}` +
` membershipEvents from storage for room ${roomId} ...`);
return events;
});
},
/**
* Stores the out-of-band membership events for this room. Note that
* it still makes sense to store an empty array as the OOB status for the room is
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
* @param {string} roomId
* @param {event[]} membershipEvents the membership events to store
*/
setOutOfBandMembers: async function(roomId, membershipEvents) {
console.log(`LL: backend about to store ${membershipEvents.length}` +
` members for ${roomId}`);
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
const store = tx.objectStore("oob_membership_events");
membershipEvents.forEach((e) => {
store.put(e);
});
// aside from all the events, we also write a marker object to the store
// to mark the fact that OOB members have been written for this room.
// It's possible that 0 members need to be written as all where previously know
// but we still need to know whether to return null or [] from getOutOfBandMembers
// where null means out of band members haven't been stored yet for this room
const markerObject = {
room_id: roomId,
oob_written: true,
state_key: 0,
};
store.put(markerObject);
await txnAsPromise(tx);
console.log(`LL: backend done storing for ${roomId}!`);
},
clearOutOfBandMembers: async function(roomId) {
// the approach to delete all members for a room
// is to get the min and max state key from the index
// for that room, and then delete between those
// keys in the store.
// this should be way faster than deleting every member
// individually for a large room.
const readTx = this.db.transaction(
["oob_membership_events"],
"readonly");
const store = readTx.objectStore("oob_membership_events");
const roomIndex = store.index("room");
const roomRange = IDBKeyRange.only(roomId);
const minStateKeyProm = reqAsCursorPromise(
roomIndex.openKeyCursor(roomRange, "next"),
).then((cursor) => cursor && cursor.primaryKey[1]);
const maxStateKeyProm = reqAsCursorPromise(
roomIndex.openKeyCursor(roomRange, "prev"),
).then((cursor) => cursor && cursor.primaryKey[1]);
const [minStateKey, maxStateKey] = await Promise.all(
[minStateKeyProm, maxStateKeyProm]);
const writeTx = this.db.transaction(
["oob_membership_events"],
"readwrite");
const writeStore = writeTx.objectStore("oob_membership_events");
const membersKeyRange = IDBKeyRange.bound(
[roomId, minStateKey],
[roomId, maxStateKey],
);
console.log(`LL: Deleting all users + marker in storage for ` +
`room ${roomId}, with key range:`,
[roomId, minStateKey], [roomId, maxStateKey]);
await reqAsPromise(writeStore.delete(membersKeyRange));
},
/**
* Clear the entire database. This should be used when logging out of a client
* to prevent mixing data between accounts.
* @return {Promise} Resolved when the database is cleared.
*/
clearDatabase: function() {
return new Promise((resolve, reject) => {
console.log(`Removing indexeddb instance: ${this._dbName}`);
const req = this.indexedDB.deleteDatabase(this._dbName);
req.onblocked = () => {
console.log(
`can't yet delete indexeddb ${this._dbName}` +
` because it is open elsewhere`,
);
};
req.onerror = (ev) => {
// in firefox, with indexedDB disabled, this fails with a
// DOMError. We treat this as non-fatal, so that we can still
// use the app.
console.warn(
`unable to delete js-sdk store indexeddb: ${ev.target.error}`,
);
resolve();
};
req.onsuccess = () => {
console.log(`Removed indexeddb instance: ${this._dbName}`);
resolve();
};
});
},
/**
* @param {boolean=} copy If false, the data returned is from internal
* buffers and must not be mutated. Otherwise, a copy is made before
* returning such that the data can be safely mutated. Default: true.
*
* @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
getSavedSync: function(copy) {
if (copy === undefined) copy = true;
const data = this._syncAccumulator.getJSON();
if (!data.nextBatch) return Promise.resolve(null);
if (copy) {
// We must deep copy the stored data so that the /sync processing code doesn't
// corrupt the internal state of the sync accumulator (it adds non-clonable keys)
return Promise.resolve(utils.deepCopy(data));
} else {
return Promise.resolve(data);
}
},
getNextBatchToken: function() {
return Promise.resolve(this._syncAccumulator.getNextBatchToken());
},
setSyncData: function(syncData) {
return Promise.resolve().then(() => {
this._syncAccumulator.accumulate(syncData);
});
},
syncToDatabase: function(userTuples) {
const syncData = this._syncAccumulator.getJSON();
return Promise.all([
this._persistUserPresenceEvents(userTuples),
this._persistAccountData(syncData.accountData),
this._persistSyncData(
syncData.nextBatch, syncData.roomsData, syncData.groupsData,
),
]);
},
/**
* Persist rooms /sync data along with the next batch token.
* @param {string} nextBatch The next_batch /sync value.
* @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator
* @param {Object} groupsData The 'groups' /sync data from a SyncAccumulator
* @return {Promise} Resolves if the data was persisted.
*/
_persistSyncData: function(nextBatch, roomsData, groupsData) {
console.log("Persisting sync data up to ", nextBatch);
return Promise.try(() => {
const txn = this.db.transaction(["sync"], "readwrite");
const store = txn.objectStore("sync");
store.put({
clobber: "-", // constant key so will always clobber
nextBatch: nextBatch,
roomsData: roomsData,
groupsData: groupsData,
}); // put == UPSERT
return txnAsPromise(txn);
});
},
/**
* Persist a list of account data events. Events with the same 'type' will
* be replaced.
* @param {Object[]} accountData An array of raw user-scoped account data events
* @return {Promise} Resolves if the events were persisted.
*/
_persistAccountData: function(accountData) {
return Promise.try(() => {
const txn = this.db.transaction(["accountData"], "readwrite");
const store = txn.objectStore("accountData");
for (let i = 0; i < accountData.length; i++) {
store.put(accountData[i]); // put == UPSERT
}
return txnAsPromise(txn);
});
},
/**
* Persist a list of [user id, presence event] they are for.
* Users with the same 'userId' will be replaced.
* Presence events should be the event in its raw form (not the Event
* object)
* @param {Object[]} tuples An array of [userid, event] tuples
* @return {Promise} Resolves if the users were persisted.
*/
_persistUserPresenceEvents: function(tuples) {
return Promise.try(() => {
const txn = this.db.transaction(["users"], "readwrite");
const store = txn.objectStore("users");
for (const tuple of tuples) {
store.put({
userId: tuple[0],
event: tuple[1],
}); // put == UPSERT
}
return txnAsPromise(txn);
});
},
/**
* Load all user presence events from the database. This is not cached.
* FIXME: It would probably be more sensible to store the events in the
* sync.
* @return {Promise<Object[]>} A list of presence events in their raw form.
*/
getUserPresenceEvents: function() {
return Promise.try(() => {
const txn = this.db.transaction(["users"], "readonly");
const store = txn.objectStore("users");
return selectQuery(store, undefined, (cursor) => {
return [cursor.value.userId, cursor.value.event];
});
});
},
/**
* Load all the account data events from the database. This is not cached.
* @return {Promise<Object[]>} A list of raw global account events.
*/
_loadAccountData: function() {
console.log(
`LocalIndexedDBStoreBackend: loading account data...`,
);
return Promise.try(() => {
const txn = this.db.transaction(["accountData"], "readonly");
const store = txn.objectStore("accountData");
return selectQuery(store, undefined, (cursor) => {
return cursor.value;
}).then((result) => {
console.log(
`LocalIndexedDBStoreBackend: loaded account data`,
);
return result;
});
});
},
/**
* Load the sync data from the database.
* @return {Promise<Object>} An object with "roomsData" and "nextBatch" keys.
*/
_loadSyncData: function() {
console.log(
`LocalIndexedDBStoreBackend: loading sync data...`,
);
return Promise.try(() => {
const txn = this.db.transaction(["sync"], "readonly");
const store = txn.objectStore("sync");
return selectQuery(store, undefined, (cursor) => {
return cursor.value;
}).then((results) => {
console.log(
`LocalIndexedDBStoreBackend: loaded sync data`,
);
if (results.length > 1) {
console.warn("loadSyncData: More than 1 sync row found.");
}
return (results.length > 0 ? results[0] : {});
});
});
},
getClientOptions: function() {
return Promise.resolve().then(() => {
const txn = this.db.transaction(["client_options"], "readonly");
const store = txn.objectStore("client_options");
return selectQuery(store, undefined, (cursor) => {
if (cursor.value && cursor.value && cursor.value.options) {
return cursor.value.options;
}
}).then((results) => results[0]);
});
},
storeClientOptions: async function(options) {
const txn = this.db.transaction(["client_options"], "readwrite");
const store = txn.objectStore("client_options");
store.put({
clobber: "-", // constant key so will always clobber
options: options,
}); // put == UPSERT
await txnAsPromise(txn);
},
};
export default LocalIndexedDBStoreBackend;
+197
View File
@@ -0,0 +1,197 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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 Promise from 'bluebird';
/**
* An IndexedDB store backend where the actual backend sits in a web
* worker.
*
* Construct a new Indexed Database store backend. This requires a call to
* <code>connect()</code> before this store can be used.
* @constructor
* @param {string} workerScript URL to the worker script
* @param {string=} dbName Optional database name. The same name must be used
* to open the same database.
* @param {Object} workerApi The web worker compatible interface object
*/
const RemoteIndexedDBStoreBackend = function RemoteIndexedDBStoreBackend(
workerScript, dbName, workerApi,
) {
this._workerScript = workerScript;
this._dbName = dbName;
this._workerApi = workerApi;
this._worker = null;
this._nextSeq = 0;
// The currently in-flight requests to the actual backend
this._inFlight = {
// seq: promise,
};
// Once we start connecting, we keep the promise and re-use it
// if we try to connect again
this._startPromise = null;
};
RemoteIndexedDBStoreBackend.prototype = {
/**
* Attempt to connect to the database. This can fail if the user does not
* grant permission.
* @return {Promise} Resolves if successfully connected.
*/
connect: function() {
return this._ensureStarted().then(() => this._doCmd('connect'));
},
/**
* Clear the entire database. This should be used when logging out of a client
* to prevent mixing data between accounts.
* @return {Promise} Resolved when the database is cleared.
*/
clearDatabase: function() {
return this._ensureStarted().then(() => this._doCmd('clearDatabase'));
},
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return this._doCmd('isNewlyCreated');
},
/**
* @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
getSavedSync: function() {
return this._doCmd('getSavedSync');
},
getNextBatchToken: function() {
return this._doCmd('getNextBatchToken');
},
setSyncData: function(syncData) {
return this._doCmd('setSyncData', [syncData]);
},
syncToDatabase: function(users) {
return this._doCmd('syncToDatabase', [users]);
},
/**
* Returns the out-of-band membership events for this room that
* were previously loaded.
* @param {string} roomId
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
getOutOfBandMembers: function(roomId) {
return this._doCmd('getOutOfBandMembers', [roomId]);
},
/**
* Stores the out-of-band membership events for this room. Note that
* it still makes sense to store an empty array as the OOB status for the room is
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
* @param {string} roomId
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
setOutOfBandMembers: function(roomId, membershipEvents) {
return this._doCmd('setOutOfBandMembers', [roomId, membershipEvents]);
},
clearOutOfBandMembers: function(roomId) {
return this._doCmd('clearOutOfBandMembers', [roomId]);
},
getClientOptions: function() {
return this._doCmd('getClientOptions');
},
storeClientOptions: function(options) {
return this._doCmd('storeClientOptions', [options]);
},
/**
* Load all user presence events from the database. This is not cached.
* @return {Promise<Object[]>} A list of presence events in their raw form.
*/
getUserPresenceEvents: function() {
return this._doCmd('getUserPresenceEvents');
},
_ensureStarted: function() {
if (this._startPromise === null) {
this._worker = new this._workerApi(this._workerScript);
this._worker.onmessage = this._onWorkerMessage.bind(this);
// tell the worker the db name.
this._startPromise = this._doCmd('_setupWorker', [this._dbName]).then(() => {
console.log("IndexedDB worker is ready");
});
}
return this._startPromise;
},
_doCmd: function(cmd, args) {
// wrap in a q so if the postMessage throws,
// the promise automatically gets rejected
return Promise.resolve().then(() => {
const seq = this._nextSeq++;
const def = Promise.defer();
this._inFlight[seq] = def;
this._worker.postMessage({
command: cmd,
seq: seq,
args: args,
});
return def.promise;
});
},
_onWorkerMessage: function(ev) {
const msg = ev.data;
if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') {
if (msg.seq === undefined) {
console.error("Got reply from worker with no seq");
return;
}
const def = this._inFlight[msg.seq];
if (def === undefined) {
console.error("Got reply for unknown seq " + msg.seq);
return;
}
delete this._inFlight[msg.seq];
if (msg.command == 'cmd_success') {
def.resolve(msg.result);
} else {
const error = new Error(msg.error.message);
error.name = msg.error.name;
def.reject(error);
}
} else {
console.warn("Unrecognised message from worker: " + msg);
}
},
};
export default RemoteIndexedDBStoreBackend;
+147
View File
@@ -0,0 +1,147 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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 Promise from 'bluebird';
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
/**
* This class lives in the webworker and drives a LocalIndexedDBStoreBackend
* controlled by messages from the main process.
*
* It should be instantiated by a web worker script provided by the application
* in a script, for example:
*
* import {IndexedDBStoreWorker} from 'matrix-js-sdk/lib/indexeddb-worker.js';
* const remoteWorker = new IndexedDBStoreWorker(postMessage);
* onmessage = remoteWorker.onMessage;
*
* Note that it is advisable to import this class by referencing the file directly to
* avoid a dependency on the whole js-sdk.
*
*/
class IndexedDBStoreWorker {
/**
* @param {function} postMessage The web worker postMessage function that
* should be used to communicate back to the main script.
*/
constructor(postMessage) {
this.backend = null;
this.postMessage = postMessage;
this.onMessage = this.onMessage.bind(this);
}
/**
* Passes a message event from the main script into the class. This method
* can be directly assigned to the web worker `onmessage` variable.
*
* @param {Object} ev The message event
*/
onMessage(ev) {
const msg = ev.data;
let prom;
switch (msg.command) {
case '_setupWorker':
this.backend = new LocalIndexedDBStoreBackend(
// this is the 'indexedDB' global (where global != window
// because it's a web worker and there is no window).
indexedDB, msg.args[0],
);
prom = Promise.resolve();
break;
case 'connect':
prom = this.backend.connect();
break;
case 'isNewlyCreated':
prom = this.backend.isNewlyCreated();
break;
case 'clearDatabase':
prom = this.backend.clearDatabase().then((result) => {
// This returns special classes which can't be cloned
// across to the main script, so don't try.
return {};
});
break;
case 'getSavedSync':
prom = this.backend.getSavedSync(false);
break;
case 'setSyncData':
prom = this.backend.setSyncData(...msg.args);
break;
case 'syncToDatabase':
prom = this.backend.syncToDatabase(...msg.args).then(() => {
// This also returns IndexedDB events which are not cloneable
return {};
});
break;
case 'getUserPresenceEvents':
prom = this.backend.getUserPresenceEvents();
break;
case 'getNextBatchToken':
prom = this.backend.getNextBatchToken();
break;
case 'getOutOfBandMembers':
prom = this.backend.getOutOfBandMembers(msg.args[0]);
break;
case 'clearOutOfBandMembers':
prom = this.backend.clearOutOfBandMembers(msg.args[0]);
break;
case 'setOutOfBandMembers':
prom = this.backend.setOutOfBandMembers(msg.args[0], msg.args[1]);
break;
case 'getClientOptions':
prom = this.backend.getClientOptions();
break;
case 'storeClientOptions':
prom = this.backend.storeClientOptions(msg.args[0]);
break;
}
if (prom === undefined) {
this.postMessage({
command: 'cmd_fail',
seq: msg.seq,
// Can't be an Error because they're not structured cloneable
error: "Unrecognised command",
});
return;
}
prom.done((ret) => {
this.postMessage.call(null, {
command: 'cmd_success',
seq: msg.seq,
result: ret,
});
}, (err) => {
console.error("Error running command: "+msg.command);
console.error(err);
this.postMessage.call(null, {
command: 'cmd_fail',
seq: msg.seq,
// Just send a string because Error objects aren't cloneable
error: {
message: err.message,
name: err.name,
},
});
});
}
}
module.exports = IndexedDBStoreWorker;
+262
View File
@@ -0,0 +1,262 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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 Promise from 'bluebird';
import {MatrixInMemoryStore} from "./memory";
import utils from "../utils";
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
import RemoteIndexedDBStoreBackend from "./indexeddb-remote-backend.js";
import User from "../models/user";
import {MatrixEvent} from "../models/event";
/**
* This is an internal module. See {@link IndexedDBStore} for the public class.
* @module store/indexeddb
*/
// If this value is too small we'll be writing very often which will cause
// noticable stop-the-world pauses. If this value is too big we'll be writing
// so infrequently that the /sync size gets bigger on reload. Writing more
// often does not affect the length of the pause since the entire /sync
// response is persisted each time.
const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
/**
* Construct a new Indexed Database store, which extends MatrixInMemoryStore.
*
* This store functions like a MatrixInMemoryStore except it periodically persists
* the contents of the store to an IndexedDB backend.
*
* All data is still kept in-memory but can be loaded from disk by calling
* <code>startup()</code>. This can make startup times quicker as a complete
* sync from the server is not required. This does not reduce memory usage as all
* the data is eagerly fetched when <code>startup()</code> is called.
* <pre>
* let opts = { localStorage: window.localStorage };
* let store = new IndexedDBStore();
* await store.startup(); // load from indexed db
* let client = sdk.createClient({
* store: store,
* });
* client.startClient();
* client.on("sync", function(state, prevState, data) {
* if (state === "PREPARED") {
* console.log("Started up, now with go faster stripes!");
* }
* });
* </pre>
*
* @constructor
* @extends MatrixInMemoryStore
* @param {Object} opts Options object.
* @param {Object} opts.indexedDB The Indexed DB interface e.g.
* <code>window.indexedDB</code>
* @param {string=} opts.dbName Optional database name. The same name must be used
* to open the same database.
* @param {string=} opts.workerScript Optional URL to a script to invoke a web
* worker with to run IndexedDB queries on the web worker. The IndexedDbStoreWorker
* class is provided for this purpose and requires the application to provide a
* trivial wrapper script around it.
* @param {Object=} opts.workerApi The webWorker API object. If omitted, the global Worker
* object will be used if it exists.
* @prop {IndexedDBStoreBackend} backend The backend instance. Call through to
* this API if you need to perform specific indexeddb actions like deleting the
* database.
*/
const IndexedDBStore = function IndexedDBStore(opts) {
MatrixInMemoryStore.call(this, opts);
if (!opts.indexedDB) {
throw new Error('Missing required option: indexedDB');
}
if (opts.workerScript) {
// try & find a webworker-compatible API
let workerApi = opts.workerApi;
if (!workerApi) {
// default to the global Worker object (which is where it in a browser)
workerApi = global.Worker;
}
this.backend = new RemoteIndexedDBStoreBackend(
opts.workerScript, opts.dbName, workerApi,
);
} else {
this.backend = new LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName);
}
this.startedUp = false;
this._syncTs = 0;
// Records the last-modified-time of each user at the last point we saved
// the database, such that we can derive the set if users that have been
// modified since we last saved.
this._userModifiedMap = {
// user_id : timestamp
};
};
utils.inherits(IndexedDBStore, MatrixInMemoryStore);
/**
* @return {Promise} Resolved when loaded from indexed db.
*/
IndexedDBStore.prototype.startup = function() {
if (this.startedUp) {
console.log(`IndexedDBStore.startup: already started`);
return Promise.resolve();
}
console.log(`IndexedDBStore.startup: connecting to backend`);
return this.backend.connect().then(() => {
console.log(`IndexedDBStore.startup: loading presence events`);
return this.backend.getUserPresenceEvents();
}).then((userPresenceEvents) => {
console.log(`IndexedDBStore.startup: processing presence events`);
userPresenceEvents.forEach(([userId, rawEvent]) => {
const u = new User(userId);
if (rawEvent) {
u.setPresenceEvent(new MatrixEvent(rawEvent));
}
this._userModifiedMap[u.userId] = u.getLastModifiedTime();
this.storeUser(u);
});
});
};
/**
* @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
IndexedDBStore.prototype.getSavedSync = function() {
return this.backend.getSavedSync();
};
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
IndexedDBStore.prototype.isNewlyCreated = function() {
return this.backend.isNewlyCreated();
};
/**
* @return {Promise} If there is a saved sync, the nextBatch token
* for this sync, otherwise null.
*/
IndexedDBStore.prototype.getSavedSyncToken = function() {
return this.backend.getNextBatchToken();
},
/**
* Delete all data from this store.
* @return {Promise} Resolves if the data was deleted from the database.
*/
IndexedDBStore.prototype.deleteAllData = function() {
MatrixInMemoryStore.prototype.deleteAllData.call(this);
return this.backend.clearDatabase().then(() => {
console.log("Deleted indexeddb data.");
}, (err) => {
console.error(`Failed to delete indexeddb data: ${err}`);
throw err;
});
};
/**
* Whether this store would like to save its data
* Note that obviously whether the store wants to save or
* not could change between calling this function and calling
* save().
*
* @return {boolean} True if calling save() will actually save
* (at the time this function is called).
*/
IndexedDBStore.prototype.wantsSave = function() {
const now = Date.now();
return now - this._syncTs > WRITE_DELAY_MS;
};
/**
* Possibly write data to the database.
* @return {Promise} Promise resolves after the write completes
* (or immediately if no write is performed)
*/
IndexedDBStore.prototype.save = function() {
if (this.wantsSave()) {
return this._reallySave();
}
return Promise.resolve();
};
IndexedDBStore.prototype._reallySave = function() {
this._syncTs = Date.now(); // set now to guard against multi-writes
// work out changed users (this doesn't handle deletions but you
// can't 'delete' users as they are just presence events).
const userTuples = [];
for (const u of this.getUsers()) {
if (this._userModifiedMap[u.userId] === u.getLastModifiedTime()) continue;
if (!u.events.presence) continue;
userTuples.push([u.userId, u.events.presence.event]);
// note that we've saved this version of the user
this._userModifiedMap[u.userId] = u.getLastModifiedTime();
}
return this.backend.syncToDatabase(userTuples).catch((err) => {
console.error("sync fail:", err);
});
};
IndexedDBStore.prototype.setSyncData = function(syncData) {
return this.backend.setSyncData(syncData);
};
/**
* Returns the out-of-band membership events for this room that
* were previously loaded.
* @param {string} roomId
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
IndexedDBStore.prototype.getOutOfBandMembers = function(roomId) {
return this.backend.getOutOfBandMembers(roomId);
};
/**
* Stores the out-of-band membership events for this room. Note that
* it still makes sense to store an empty array as the OOB status for the room is
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
* @param {string} roomId
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
IndexedDBStore.prototype.setOutOfBandMembers = function(roomId, membershipEvents) {
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
};
IndexedDBStore.prototype.clearOutOfBandMembers = function(roomId) {
return this.backend.clearOutOfBandMembers(roomId);
};
IndexedDBStore.prototype.getClientOptions = function() {
return this.backend.getClientOptions();
};
IndexedDBStore.prototype.storeClientOptions = function(options) {
return this.backend.storeClientOptions(options);
};
module.exports.IndexedDBStore = IndexedDBStore;
+142 -6
View File
@@ -1,5 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 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.
@@ -18,8 +20,9 @@ limitations under the License.
* This is an internal module. See {@link MatrixInMemoryStore} for the public class.
* @module store/memory
*/
const utils = require("../utils");
const User = require("../models/user");
const utils = require("../utils");
const User = require("../models/user");
import Promise from 'bluebird';
/**
* Construct a new in-memory data store for the Matrix Client.
@@ -33,6 +36,9 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
this.rooms = {
// roomId: Room
};
this.groups = {
// groupId: Group
};
this.users = {
// userId: User
};
@@ -46,6 +52,10 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
// type : content
};
this.localStorage = opts.localStorage;
this._oobMembers = {
// roomId: [member events]
};
this._clientOptions = {};
};
module.exports.MatrixInMemoryStore.prototype = {
@@ -58,6 +68,10 @@ module.exports.MatrixInMemoryStore.prototype = {
return this.syncToken;
},
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return Promise.resolve(true);
},
/**
* Set the token to stream from.
@@ -67,6 +81,31 @@ module.exports.MatrixInMemoryStore.prototype = {
this.syncToken = token;
},
/**
* Store the given room.
* @param {Group} group The group to be stored
*/
storeGroup: function(group) {
this.groups[group.groupId] = group;
},
/**
* Retrieve a group by its group ID.
* @param {string} groupId The group ID.
* @return {Group} The group or null.
*/
getGroup: function(groupId) {
return this.groups[groupId] || null;
},
/**
* Retrieve all known groups.
* @return {Group[]} A list of groups, which may be empty.
*/
getGroups: function() {
return utils.values(this.groups);
},
/**
* Store the given room.
* @param {Room} room The room to be stored. All properties must be stored.
@@ -275,9 +314,106 @@ module.exports.MatrixInMemoryStore.prototype = {
return this.accountData[eventType];
},
// TODO
//setMaxHistoryPerRoom: function(maxHistory) {},
/**
* setSyncData does nothing as there is no backing data store.
*
* @param {Object} syncData The sync data
* @return {Promise} An immediately resolved promise.
*/
setSyncData: function(syncData) {
return Promise.resolve();
},
// TODO
//reapOldMessages: function() {},
/**
* We never want to save becase we have nothing to save to.
*
* @return {boolean} If the store wants to save
*/
wantsSave: function() {
return false;
},
/**
* Save does nothing as there is no backing data store.
*/
save: function() {},
/**
* Startup does nothing as this store doesn't require starting up.
* @return {Promise} An immediately resolved promise.
*/
startup: function() {
return Promise.resolve();
},
/**
* @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
getSavedSync: function() {
return Promise.resolve(null);
},
/**
* @return {Promise} If there is a saved sync, the nextBatch token
* for this sync, otherwise null.
*/
getSavedSyncToken: function() {
return Promise.resolve(null);
},
/**
* Delete all data from this store.
* @return {Promise} An immediately resolved promise.
*/
deleteAllData: function() {
this.rooms = {
// roomId: Room
};
this.users = {
// userId: User
};
this.syncToken = null;
this.filters = {
// userId: {
// filterId: Filter
// }
};
this.accountData = {
// type : content
};
return Promise.resolve();
},
/**
* Returns the out-of-band membership events for this room that
* were previously loaded.
* @param {string} roomId
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
getOutOfBandMembers: function(roomId) {
return Promise.resolve(this._oobMembers[roomId] || null);
},
/**
* Stores the out-of-band membership events for this room. Note that
* it still makes sense to store an empty array as the OOB status for the room is
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
* @param {string} roomId
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
setOutOfBandMembers: function(roomId, membershipEvents) {
this._oobMembers[roomId] = membershipEvents;
return Promise.resolve();
},
getClientOptions: function() {
return Promise.resolve(this._clientOptions);
},
storeClientOptions: function(options) {
this._clientOptions = Object.assign({}, options);
return Promise.resolve();
},
};
+82 -73
View File
@@ -1,5 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 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.
@@ -48,17 +50,18 @@ function WebStorageSessionStore(webStore) {
}
WebStorageSessionStore.prototype = {
/**
* Store the end to end account for the logged-in user.
* @param {string} account Base64 encoded account.
* Remove the stored end to end account for the logged-in user.
*/
storeEndToEndAccount: function(account) {
this.store.setItem(KEY_END_TO_END_ACCOUNT, account);
removeEndToEndAccount: function() {
this.store.removeItem(KEY_END_TO_END_ACCOUNT);
},
/**
* Load the end to end account for the logged-in user.
* Note that the end-to-end account is now stored in the
* crypto store rather than here: this remains here so
* old sessions can be migrated out of the session store.
* @return {?string} Base64 encoded account.
*/
getEndToEndAccount: function() {
@@ -66,49 +69,22 @@ WebStorageSessionStore.prototype = {
},
/**
* Store a flag indicating that we have announced the new device.
* Retrieves the known devices for all users.
* @return {object} A map from user ID to map of device ID to keys for the device.
*/
setDeviceAnnounced: function() {
this.store.setItem(KEY_END_TO_END_ANNOUNCED, "true");
getAllEndToEndDevices: function() {
const prefix = keyEndToEndDevicesForUser('');
const devices = {};
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
const userId = key.substr(prefix.length);
if (key.startsWith(prefix)) devices[userId] = getJsonItem(this.store, key);
}
return devices;
},
/**
* Check if the "device announced" flag is set
*
* @return {boolean} true if the "device announced" flag has been set.
*/
getDeviceAnnounced: function() {
return this.store.getItem(KEY_END_TO_END_ANNOUNCED) == "true";
},
/**
* Stores the known devices for a user.
* @param {string} userId The user's ID.
* @param {object} devices A map from device ID to keys for the device.
*/
storeEndToEndDevicesForUser: function(userId, devices) {
setJsonItem(this.store, keyEndToEndDevicesForUser(userId), devices);
},
/**
* Retrieves the known devices for a user.
* @param {string} userId The user's ID.
* @return {object} A map from device ID to keys for the device.
*/
getEndToEndDevicesForUser: function(userId) {
return getJsonItem(this.store, keyEndToEndDevicesForUser(userId));
},
/**
* Store the sync token corresponding to the device list.
*
* This is used when starting the client, to get a list of the users who
* have changed their device list since the list time we were running.
*
* @param {String?} token
*/
storeEndToEndDeviceSyncToken: function(token) {
setJsonItem(this.store, KEY_END_TO_END_DEVICE_SYNC_TOKEN, token);
getEndToEndDeviceTrackingStatus: function() {
return getJsonItem(this.store, KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS);
},
/**
@@ -121,17 +97,12 @@ WebStorageSessionStore.prototype = {
},
/**
* Store a session between the logged-in user and another device
* @param {string} deviceKey The public key of the other device.
* @param {string} sessionId The ID for this end-to-end session.
* @param {string} session Base64 encoded end-to-end session.
* Removes all end to end device data from the store
*/
storeEndToEndSession: function(deviceKey, sessionId, session) {
const sessions = this.getEndToEndSessions(deviceKey) || {};
sessions[sessionId] = session;
setJsonItem(
this.store, keyEndToEndSessions(deviceKey), sessions,
);
removeEndToEndDeviceData: function() {
removeByPrefix(this.store, keyEndToEndDevicesForUser(''));
removeByPrefix(this.store, KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS);
removeByPrefix(this.store, KEY_END_TO_END_DEVICE_SYNC_TOKEN);
},
/**
@@ -144,6 +115,29 @@ WebStorageSessionStore.prototype = {
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
},
/**
* Retrieve all end-to-end sessions between the logged-in user and other
* devices.
* @return {object} A map of {deviceKey -> {sessionId -> session pickle}}
*/
getAllEndToEndSessions: function() {
const deviceKeys = getKeysWithPrefix(this.store, keyEndToEndSessions(''));
const results = {};
for (const k of deviceKeys) {
const unprefixedKey = k.substr(keyEndToEndSessions('').length);
results[unprefixedKey] = getJsonItem(this.store, k);
}
return results;
},
/**
* Remove all end-to-end sessions from the store
* This is used after migrating sessions awat from the sessions store.
*/
removeAllEndToEndSessions: function() {
removeByPrefix(this.store, keyEndToEndSessions(''));
},
/**
* Retrieve a list of all known inbound group sessions
*
@@ -175,33 +169,32 @@ WebStorageSessionStore.prototype = {
return this.store.getItem(key);
},
storeEndToEndInboundGroupSession: function(senderKey, sessionId, pickledSession) {
const key = keyEndToEndInboundGroupSession(senderKey, sessionId);
return this.store.setItem(key, pickledSession);
removeAllEndToEndInboundGroupSessions: function() {
removeByPrefix(this.store, E2E_PREFIX + 'inboundgroupsessions/');
},
/**
* Store the end-to-end state for a room.
* @param {string} roomId The room's ID.
* @param {object} roomInfo The end-to-end info for the room.
* Get the end-to-end state for all rooms
* @return {object} roomId -> object with the end-to-end info for the room.
*/
storeEndToEndRoom: function(roomId, roomInfo) {
setJsonItem(this.store, keyEndToEndRoom(roomId), roomInfo);
getAllEndToEndRooms: function() {
const roomKeys = getKeysWithPrefix(this.store, keyEndToEndRoom(''));
const results = {};
for (const k of roomKeys) {
const unprefixedKey = k.substr(keyEndToEndRoom('').length);
results[unprefixedKey] = getJsonItem(this.store, k);
}
return results;
},
/**
* Get the end-to-end state for a room
* @param {string} roomId The room's ID.
* @return {object} The end-to-end info for the room.
*/
getEndToEndRoom: function(roomId) {
return getJsonItem(this.store, keyEndToEndRoom(roomId));
removeAllEndToEndRooms: function() {
removeByPrefix(this.store, keyEndToEndRoom(''));
},
};
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";
const KEY_END_TO_END_DEVICE_SYNC_TOKEN = E2E_PREFIX + "device_sync_token";
const KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS = E2E_PREFIX + "device_tracking";
function keyEndToEndDevicesForUser(userId) {
return E2E_PREFIX + "devices/" + userId;
@@ -231,8 +224,24 @@ function getJsonItem(store, key) {
return null;
}
function setJsonItem(store, key, val) {
store.setItem(key, JSON.stringify(val));
function getKeysWithPrefix(store, prefix) {
const results = [];
for (let i = 0; i < store.length; ++i) {
const key = store.key(i);
if (key.startsWith(prefix)) results.push(key);
}
return results;
}
function removeByPrefix(store, prefix) {
const toRemove = [];
for (let i = 0; i < store.length; ++i) {
const key = store.key(i);
if (key.startsWith(prefix)) toRemove.push(key);
}
for (const key of toRemove) {
store.removeItem(key);
}
}
function debuglog() {
+108 -4
View File
@@ -1,5 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 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.
@@ -14,6 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import Promise from 'bluebird';
/**
* This is an internal module.
* @module store/stub
@@ -29,6 +32,11 @@ function StubStore() {
StubStore.prototype = {
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return Promise.resolve(true);
},
/**
* Get the sync token.
* @return {string}
@@ -45,6 +53,30 @@ StubStore.prototype = {
this.fromToken = token;
},
/**
* No-op.
* @param {Group} group
*/
storeGroup: function(group) {
},
/**
* No-op.
* @param {string} groupId
* @return {null}
*/
getGroup: function(groupId) {
return null;
},
/**
* No-op.
* @return {Array} An empty array.
*/
getGroups: function() {
return [];
},
/**
* No-op.
* @param {Room} room
@@ -180,11 +212,83 @@ StubStore.prototype = {
},
// TODO
//setMaxHistoryPerRoom: function(maxHistory) {},
/**
* setSyncData does nothing as there is no backing data store.
*
* @param {Object} syncData The sync data
* @return {Promise} An immediately resolved promise.
*/
setSyncData: function(syncData) {
return Promise.resolve();
},
// TODO
//reapOldMessages: function() {},
/**
* We never want to save becase we have nothing to save to.
*
* @return {boolean} If the store wants to save
*/
wantsSave: function() {
return false;
},
/**
* Save does nothing as there is no backing data store.
*/
save: function() {},
/**
* Startup does nothing.
* @return {Promise} An immediately resolved promise.
*/
startup: function() {
return Promise.resolve();
},
/**
* @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
getSavedSync: function() {
return Promise.resolve(null);
},
/**
* @return {Promise} If there is a saved sync, the nextBatch token
* for this sync, otherwise null.
*/
getSavedSyncToken: function() {
return Promise.resolve(null);
},
/**
* Delete all data from this store. Does nothing since this store
* doesn't store anything.
* @return {Promise} An immediately resolved promise.
*/
deleteAllData: function() {
return Promise.resolve();
},
getOutOfBandMembers: function() {
return Promise.resolve(null);
},
setOutOfBandMembers: function() {
return Promise.resolve();
},
clearOutOfBandMembers: function() {
return Promise.resolve();
},
getClientOptions: function() {
return Promise.resolve();
},
storeClientOptions: function() {
return Promise.resolve();
},
};
/** Stub Store class. */
+558
View File
@@ -0,0 +1,558 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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.
*/
/**
* This is an internal module. See {@link SyncAccumulator} for the public class.
* @module sync-accumulator
*/
import utils from "./utils";
/**
* The purpose of this class is to accumulate /sync responses such that a
* complete "initial" JSON response can be returned which accurately represents
* the sum total of the /sync responses accumulated to date. It only handles
* room data: that is, everything under the "rooms" top-level key.
*
* This class is used when persisting room data so a complete /sync response can
* be loaded from disk and incremental syncs can be performed on the server,
* rather than asking the server to do an initial sync on startup.
*/
class SyncAccumulator {
/**
* @param {Object} opts
* @param {Number=} opts.maxTimelineEntries The ideal maximum number of
* timeline entries to keep in the sync response. This is best-effort, as
* clients do not always have a back-pagination token for each event, so
* it's possible there may be slightly *less* than this value. There will
* never be more. This cannot be 0 or else it makes it impossible to scroll
* back in a room. Default: 50.
*/
constructor(opts) {
opts = opts || {};
opts.maxTimelineEntries = opts.maxTimelineEntries || 50;
this.opts = opts;
this.accountData = {
//$event_type: Object
};
this.inviteRooms = {
//$roomId: { ... sync 'invite' json data ... }
};
this.joinRooms = {
//$roomId: {
// _currentState: { $event_type: { $state_key: json } },
// _timeline: [
// { event: $event, token: null|token },
// { event: $event, token: null|token },
// { event: $event, token: null|token },
// ...
// ],
// _summary: {
// m.heroes: [ $user_id ],
// m.joined_member_count: $count,
// m.invited_member_count: $count
// },
// _accountData: { $event_type: json },
// _unreadNotifications: { ... unread_notifications JSON ... },
// _readReceipts: { $user_id: { data: $json, eventId: $event_id }}
//}
};
// the /sync token which corresponds to the last time rooms were
// accumulated. We remember this so that any caller can obtain a
// coherent /sync response and know at what point they should be
// streaming from without losing events.
this.nextBatch = null;
// { ('invite'|'join'|'leave'): $groupId: { ... sync 'group' data } }
this.groups = {
invite: {},
join: {},
leave: {},
};
}
accumulate(syncResponse) {
this._accumulateRooms(syncResponse);
this._accumulateGroups(syncResponse);
this._accumulateAccountData(syncResponse);
this.nextBatch = syncResponse.next_batch;
}
_accumulateAccountData(syncResponse) {
if (!syncResponse.account_data || !syncResponse.account_data.events) {
return;
}
// Clobbers based on event type.
syncResponse.account_data.events.forEach((e) => {
this.accountData[e.type] = e;
});
}
/**
* Accumulate incremental /sync room data.
* @param {Object} syncResponse the complete /sync JSON
*/
_accumulateRooms(syncResponse) {
if (!syncResponse.rooms) {
return;
}
if (syncResponse.rooms.invite) {
Object.keys(syncResponse.rooms.invite).forEach((roomId) => {
this._accumulateRoom(
roomId, "invite", syncResponse.rooms.invite[roomId],
);
});
}
if (syncResponse.rooms.join) {
Object.keys(syncResponse.rooms.join).forEach((roomId) => {
this._accumulateRoom(
roomId, "join", syncResponse.rooms.join[roomId],
);
});
}
if (syncResponse.rooms.leave) {
Object.keys(syncResponse.rooms.leave).forEach((roomId) => {
this._accumulateRoom(
roomId, "leave", syncResponse.rooms.leave[roomId],
);
});
}
}
_accumulateRoom(roomId, category, data) {
// Valid /sync state transitions
// +--------+ <======+ 1: Accept an invite
// +== | INVITE | | (5) 2: Leave a room
// | +--------+ =====+ | 3: Join a public room previously
// |(1) (4) | | left (handle as if new room)
// V (2) V | 4: Reject an invite
// +------+ ========> +--------+ 5: Invite to a room previously
// | JOIN | (3) | LEAVE* | left (handle as if new room)
// +------+ <======== +--------+
//
// * equivalent to "no state"
switch (category) {
case "invite": // (5)
this._accumulateInviteState(roomId, data);
break;
case "join":
if (this.inviteRooms[roomId]) { // (1)
// was previously invite, now join. We expect /sync to give
// the entire state and timeline on 'join', so delete previous
// invite state
delete this.inviteRooms[roomId];
}
// (3)
this._accumulateJoinState(roomId, data);
break;
case "leave":
if (this.inviteRooms[roomId]) { // (4)
delete this.inviteRooms[roomId];
} else { // (2)
delete this.joinRooms[roomId];
}
break;
default:
console.error("Unknown cateogory: ", category);
}
}
_accumulateInviteState(roomId, data) {
if (!data.invite_state || !data.invite_state.events) { // no new data
return;
}
if (!this.inviteRooms[roomId]) {
this.inviteRooms[roomId] = {
invite_state: data.invite_state,
};
return;
}
// accumulate extra keys for invite->invite transitions
// clobber based on event type / state key
// We expect invite_state to be small, so just loop over the events
const currentData = this.inviteRooms[roomId];
data.invite_state.events.forEach((e) => {
let hasAdded = false;
for (let i = 0; i < currentData.invite_state.events.length; i++) {
const current = currentData.invite_state.events[i];
if (current.type === e.type && current.state_key == e.state_key) {
currentData.invite_state.events[i] = e; // update
hasAdded = true;
}
}
if (!hasAdded) {
currentData.invite_state.events.push(e);
}
});
}
// Accumulate timeline and state events in a room.
_accumulateJoinState(roomId, data) {
// We expect this function to be called a lot (every /sync) so we want
// this to be fast. /sync stores events in an array but we often want
// to clobber based on type/state_key. Rather than convert arrays to
// maps all the time, just keep private maps which contain
// the actual current accumulated sync state, and array-ify it when
// getJSON() is called.
// State resolution:
// The 'state' key is the delta from the previous sync (or start of time
// if no token was supplied), to the START of the timeline. To obtain
// the current state, we need to "roll forward" state by reading the
// timeline. We want to store the current state so we can drop events
// out the end of the timeline based on opts.maxTimelineEntries.
//
// 'state' 'timeline' current state
// |-------x<======================>x
// T I M E
//
// When getJSON() is called, we 'roll back' the current state by the
// number of entries in the timeline to work out what 'state' should be.
// Back-pagination:
// On an initial /sync, the server provides a back-pagination token for
// the start of the timeline. When /sync deltas come down, they also
// include back-pagination tokens for the start of the timeline. This
// means not all events in the timeline have back-pagination tokens, as
// it is only the ones at the START of the timeline which have them.
// In order for us to have a valid timeline (and back-pagination token
// to match), we need to make sure that when we remove old timeline
// events, that we roll forward to an event which has a back-pagination
// token. This means we can't keep a strict sliding-window based on
// opts.maxTimelineEntries, and we may have a few less. We should never
// have more though, provided that the /sync limit is less than or equal
// to opts.maxTimelineEntries.
if (!this.joinRooms[roomId]) {
// Create truly empty objects so event types of 'hasOwnProperty' and co
// don't cause this code to break.
this.joinRooms[roomId] = {
_currentState: Object.create(null),
_timeline: [],
_accountData: Object.create(null),
_unreadNotifications: {},
_summary: {},
_readReceipts: {},
};
}
const currentData = this.joinRooms[roomId];
if (data.account_data && data.account_data.events) {
// clobber based on type
data.account_data.events.forEach((e) => {
currentData._accountData[e.type] = e;
});
}
// these probably clobber, spec is unclear.
if (data.unread_notifications) {
currentData._unreadNotifications = data.unread_notifications;
}
if (data.summary) {
const HEROES_KEY = "m.heroes";
const INVITED_COUNT_KEY = "m.invited_member_count";
const JOINED_COUNT_KEY = "m.joined_member_count";
const acc = currentData._summary;
const sum = data.summary;
acc[HEROES_KEY] = sum[HEROES_KEY] || acc[HEROES_KEY];
acc[JOINED_COUNT_KEY] = sum[JOINED_COUNT_KEY] || acc[JOINED_COUNT_KEY];
acc[INVITED_COUNT_KEY] = sum[INVITED_COUNT_KEY] || acc[INVITED_COUNT_KEY];
}
if (data.ephemeral && data.ephemeral.events) {
data.ephemeral.events.forEach((e) => {
// We purposefully do not persist m.typing events.
// Technically you could refresh a browser before the timer on a
// typing event is up, so it'll look like you aren't typing when
// you really still are. However, the alternative is worse. If
// we do persist typing events, it will look like people are
// typing forever until someone really does start typing (which
// will prompt Synapse to send down an actual m.typing event to
// clobber the one we persisted).
if (e.type !== "m.receipt" || !e.content) {
// This means we'll drop unknown ephemeral events but that
// seems okay.
return;
}
// Handle m.receipt events. They clobber based on:
// (user_id, receipt_type)
// but they are keyed in the event as:
// content:{ $event_id: { $receipt_type: { $user_id: {json} }}}
// so store them in the former so we can accumulate receipt deltas
// quickly and efficiently (we expect a lot of them). Fold the
// receipt type into the key name since we only have 1 at the
// moment (m.read) and nested JSON objects are slower and more
// of a hassle to work with. We'll inflate this back out when
// getJSON() is called.
Object.keys(e.content).forEach((eventId) => {
if (!e.content[eventId]["m.read"]) {
return;
}
Object.keys(e.content[eventId]["m.read"]).forEach((userId) => {
// clobber on user ID
currentData._readReceipts[userId] = {
data: e.content[eventId]["m.read"][userId],
eventId: eventId,
};
});
});
});
}
// if we got a limited sync, we need to remove all timeline entries or else
// we will have gaps in the timeline.
if (data.timeline && data.timeline.limited) {
currentData._timeline = [];
}
// Work out the current state. The deltas need to be applied in the order:
// - existing state which didn't come down /sync.
// - State events under the 'state' key.
// - State events in the 'timeline'.
if (data.state && data.state.events) {
data.state.events.forEach((e) => {
setState(currentData._currentState, e);
});
}
if (data.timeline && data.timeline.events) {
data.timeline.events.forEach((e, index) => {
// this nops if 'e' isn't a state event
setState(currentData._currentState, e);
// append the event to the timeline. The back-pagination token
// corresponds to the first event in the timeline
currentData._timeline.push({
event: e,
token: index === 0 ? data.timeline.prev_batch : null,
});
});
}
// attempt to prune the timeline by jumping between events which have
// pagination tokens.
if (currentData._timeline.length > this.opts.maxTimelineEntries) {
const startIndex = (
currentData._timeline.length - this.opts.maxTimelineEntries
);
for (let i = startIndex; i < currentData._timeline.length; i++) {
if (currentData._timeline[i].token) {
// keep all events after this, including this one
currentData._timeline = currentData._timeline.slice(
i, currentData._timeline.length,
);
break;
}
}
}
}
/**
* Accumulate incremental /sync group data.
* @param {Object} syncResponse the complete /sync JSON
*/
_accumulateGroups(syncResponse) {
if (!syncResponse.groups) {
return;
}
if (syncResponse.groups.invite) {
Object.keys(syncResponse.groups.invite).forEach((groupId) => {
this._accumulateGroup(
groupId, "invite", syncResponse.groups.invite[groupId],
);
});
}
if (syncResponse.groups.join) {
Object.keys(syncResponse.groups.join).forEach((groupId) => {
this._accumulateGroup(
groupId, "join", syncResponse.groups.join[groupId],
);
});
}
if (syncResponse.groups.leave) {
Object.keys(syncResponse.groups.leave).forEach((groupId) => {
this._accumulateGroup(
groupId, "leave", syncResponse.groups.leave[groupId],
);
});
}
}
_accumulateGroup(groupId, category, data) {
for (const cat of ['invite', 'join', 'leave']) {
delete this.groups[cat][groupId];
}
this.groups[category][groupId] = data;
}
/**
* Return everything under the 'rooms' key from a /sync response which
* represents all room data that should be stored. This should be paired
* with the sync token which represents the most recent /sync response
* provided to accumulate().
* @return {Object} An object with a "nextBatch", "roomsData" and "accountData"
* keys.
* The "nextBatch" key is a string which represents at what point in the
* /sync stream the accumulator reached. This token should be used when
* restarting a /sync stream at startup. Failure to do so can lead to missing
* events. The "roomsData" key is an Object which represents the entire
* /sync response from the 'rooms' key onwards. The "accountData" key is
* a list of raw events which represent global account data.
*/
getJSON() {
const data = {
join: {},
invite: {},
// always empty. This is set by /sync when a room was previously
// in 'invite' or 'join'. On fresh startup, the client won't know
// about any previous room being in 'invite' or 'join' so we can
// just omit mentioning it at all, even if it has previously come
// down /sync.
// The notable exception is when a client is kicked or banned:
// we may want to hold onto that room so the client can clearly see
// why their room has disappeared. We don't persist it though because
// it is unclear *when* we can safely remove the room from the DB.
// Instead, we assume that if you're loading from the DB, you've
// refreshed the page, which means you've seen the kick/ban already.
leave: {},
};
Object.keys(this.inviteRooms).forEach((roomId) => {
data.invite[roomId] = this.inviteRooms[roomId];
});
Object.keys(this.joinRooms).forEach((roomId) => {
const roomData = this.joinRooms[roomId];
const roomJson = {
ephemeral: { events: [] },
account_data: { events: [] },
state: { events: [] },
timeline: {
events: [],
prev_batch: null,
},
unread_notifications: roomData._unreadNotifications,
summary: roomData._summary,
};
// Add account data
Object.keys(roomData._accountData).forEach((evType) => {
roomJson.account_data.events.push(roomData._accountData[evType]);
});
// Add receipt data
const receiptEvent = {
type: "m.receipt",
room_id: roomId,
content: {
// $event_id: { "m.read": { $user_id: $json } }
},
};
Object.keys(roomData._readReceipts).forEach((userId) => {
const receiptData = roomData._readReceipts[userId];
if (!receiptEvent.content[receiptData.eventId]) {
receiptEvent.content[receiptData.eventId] = {
"m.read": {},
};
}
receiptEvent.content[receiptData.eventId]["m.read"][userId] = (
receiptData.data
);
});
// add only if we have some receipt data
if (Object.keys(receiptEvent.content).length > 0) {
roomJson.ephemeral.events.push(receiptEvent);
}
// Add timeline data
roomData._timeline.forEach((msgData) => {
if (!roomJson.timeline.prev_batch) {
// the first event we add to the timeline MUST match up to
// the prev_batch token.
if (!msgData.token) {
return; // this shouldn't happen as we prune constantly.
}
roomJson.timeline.prev_batch = msgData.token;
}
roomJson.timeline.events.push(msgData.event);
});
// Add state data: roll back current state to the start of timeline,
// by "reverse clobbering" from the end of the timeline to the start.
// Convert maps back into arrays.
const rollBackState = Object.create(null);
for (let i = roomJson.timeline.events.length - 1; i >=0; i--) {
const timelineEvent = roomJson.timeline.events[i];
if (timelineEvent.state_key === null ||
timelineEvent.state_key === undefined) {
continue; // not a state event
}
// since we're going back in time, we need to use the previous
// state value else we'll break causality. We don't have the
// complete previous state event, so we need to create one.
const prevStateEvent = utils.deepCopy(timelineEvent);
if (prevStateEvent.unsigned) {
if (prevStateEvent.unsigned.prev_content) {
prevStateEvent.content = prevStateEvent.unsigned.prev_content;
}
if (prevStateEvent.unsigned.prev_sender) {
prevStateEvent.sender = prevStateEvent.unsigned.prev_sender;
}
}
setState(rollBackState, prevStateEvent);
}
Object.keys(roomData._currentState).forEach((evType) => {
Object.keys(roomData._currentState[evType]).forEach((stateKey) => {
let ev = roomData._currentState[evType][stateKey];
if (rollBackState[evType] && rollBackState[evType][stateKey]) {
// use the reverse clobbered event instead.
ev = rollBackState[evType][stateKey];
}
roomJson.state.events.push(ev);
});
});
data.join[roomId] = roomJson;
});
// Add account data
const accData = [];
Object.keys(this.accountData).forEach((evType) => {
accData.push(this.accountData[evType]);
});
return {
nextBatch: this.nextBatch,
roomsData: data,
groupsData: this.groups,
accountData: accData,
};
}
getNextBatchToken() {
return this.nextBatch;
}
}
function setState(eventMap, event) {
if (event.state_key === null || event.state_key === undefined || !event.type) {
return;
}
if (!eventMap[event.type]) {
eventMap[event.type] = Object.create(null);
}
eventMap[event.type][event.state_key] = event;
}
module.exports = SyncAccumulator;
+628 -199
View File
File diff suppressed because it is too large Load Diff
+38 -24
View File
@@ -17,7 +17,7 @@ limitations under the License.
/** @module timeline-window */
const q = require("q");
import Promise from 'bluebird';
const EventTimeline = require("./models/event-timeline");
/**
@@ -95,10 +95,30 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
const self = this;
initialWindowSize = initialWindowSize || 20;
// given an EventTimeline, and an event index within it, initialise our
// given an EventTimeline, find the event we were looking for, and initialise our
// fields so that the event in question is in the middle of the window.
const initFields = function(timeline, eventIndex) {
const endIndex = Math.min(timeline.getEvents().length,
const initFields = function(timeline) {
let eventIndex;
const events = timeline.getEvents();
if (!initialEventId) {
// we were looking for the live timeline: initialise to the end
eventIndex = events.length;
} else {
for (let i = 0; i < events.length; i++) {
if (events[i].getId() == initialEventId) {
eventIndex = i;
break;
}
}
if (eventIndex === undefined) {
throw new Error("getEventTimeline result didn't include requested event");
}
}
const endIndex = Math.min(events.length,
eventIndex + Math.ceil(initialWindowSize / 2));
const startIndex = Math.max(0, endIndex - initialWindowSize);
self._start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex());
@@ -110,25 +130,19 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
// we already have the data we need, which is important to keep room-switching
// feeling snappy.
//
// TODO: ideally we'd spot getEventTimeline returning a resolved promise and
// skip straight to the find-event loop.
if (initialEventId) {
return this._client.getEventTimeline(this._timelineSet, initialEventId)
.then(function(tl) {
// make sure that our window includes the event
for (let i = 0; i < tl.getEvents().length; i++) {
if (tl.getEvents()[i].getId() == initialEventId) {
initFields(tl, i);
return;
}
}
throw new Error("getEventTimeline result didn't include requested event");
});
const prom = this._client.getEventTimeline(this._timelineSet, initialEventId);
if (prom.isFulfilled()) {
initFields(prom.value());
return Promise.resolve();
} else {
return prom.then(initFields);
}
} else {
// start with the most recent events
const tl = this._timelineSet.getLiveTimeline();
initFields(tl, tl.getEvents().length);
return q();
initFields(tl);
return Promise.resolve();
}
};
@@ -220,7 +234,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
if (!tl) {
debuglog("TimelineWindow: no timeline yet");
return q(false);
return Promise.resolve(false);
}
if (tl.pendingPaginate) {
@@ -240,20 +254,20 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
if (excess > 0) {
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
}
return q(true);
return Promise.resolve(true);
}
if (!makeRequest || requestLimit === 0) {
// todo: should we return something different to indicate that there
// might be more events out there, but we haven't found them yet?
return q(false);
return Promise.resolve(false);
}
// try making a pagination request
const token = tl.timeline.getPaginationToken(direction);
if (!token) {
debuglog("TimelineWindow: no token");
return q(false);
return Promise.resolve(false);
}
debuglog("TimelineWindow: starting request");
+20
View File
@@ -652,3 +652,23 @@ module.exports.inherits = function(ctor, superCtor) {
},
});
};
/**
* Returns whether the given value is a finite number without type-coercion
*
* @param {*} value the value to test
* @return {boolean} whether or not value is a finite number without type-coercion
*/
module.exports.isNumber = function(value) {
return typeof value === 'number' && isFinite(value);
};
/**
* Removes zero width chars, diacritics and whitespace from the string
* @param {string} str the string to remove hidden characters from
* @return {string} a string with the hidden characters removed
*/
module.exports.removeHiddenChars = function(str) {
return str.normalize('NFD').replace(removeHiddenCharsRegex, '');
};
const removeHiddenCharsRegex = /[\u200B-\u200D\u0300-\u036f\uFEFF\s]/g;
+259 -101
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket 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.
@@ -24,12 +25,29 @@ const DEBUG = true; // set true to enable console logging.
// events: hangup, error(err), replaced(call), state(state, oldState)
/**
* Fires whenever an error occurs when call.js encounters an issue with setting up the call.
* <p>
* The error given will have a code equal to either `MatrixCall.ERR_LOCAL_OFFER_FAILED` or
* `MatrixCall.ERR_NO_USER_MEDIA`. `ERR_LOCAL_OFFER_FAILED` is emitted when the local client
* fails to create an offer. `ERR_NO_USER_MEDIA` is emitted when the user has denied access
* to their audio/video hardware.
*
* @event module:webrtc/call~MatrixCall#"error"
* @param {Error} err The error raised by MatrixCall.
* @example
* matrixCall.on("error", function(err){
* console.error(err.code, err);
* });
*/
/**
* Construct a new Matrix Call.
* @constructor
* @param {Object} opts Config options.
* @param {string} opts.roomId The room ID for this call.
* @param {Object} opts.webRtc The WebRTC globals from the browser.
* @param {boolean} opts.forceTURN whether relay through TURN should be forced.
* @param {Object} opts.URL The URL global.
* @param {Array<Object>} opts.turnServers Optional. A list of TURN servers.
* @param {MatrixClient} opts.client The Matrix Client instance to send events to.
@@ -38,6 +56,7 @@ function MatrixCall(opts) {
this.roomId = opts.roomId;
this.client = opts.client;
this.webRtc = opts.webRtc;
this.forceTURN = opts.forceTURN;
this.URL = opts.URL;
// Array of Objects with urls, username, credential keys
this.turnServers = opts.turnServers || [];
@@ -50,7 +69,7 @@ function MatrixCall(opts) {
utils.checkObjectHasKeys(server, ["urls"]);
});
this.callId = "c" + new Date().getTime();
this.callId = "c" + new Date().getTime() + Math.random();
this.state = 'fledgling';
this.didConnect = false;
@@ -67,6 +86,8 @@ function MatrixCall(opts) {
this.mediaPromises = Object.create(null);
this.screenSharingStream = null;
this._answerContent = null;
}
/** The length of time a call can be ringing for. */
MatrixCall.CALL_TIMEOUT_MS = 60000;
@@ -80,6 +101,24 @@ MatrixCall.ERR_LOCAL_OFFER_FAILED = "local_offer_failed";
*/
MatrixCall.ERR_NO_USER_MEDIA = "no_user_media";
/*
* Error code used when a call event failed to send
* because unknown devices were present in the room
*/
MatrixCall.ERR_UNKNOWN_DEVICES = "unknown_devices";
/*
* Error code usewd when we fail to send the invite
* for some reason other than there being unknown devices
*/
MatrixCall.ERR_SEND_INVITE = "send_invite";
/*
* Error code usewd when we fail to send the answer
* for some reason other than there being unknown devices
*/
MatrixCall.ERR_SEND_ANSWER = "send_answer";
utils.inherits(MatrixCall, EventEmitter);
/**
@@ -114,7 +153,7 @@ MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoEle
/**
* Place a screen-sharing call to this room. This includes audio.
* <b>This method is EXPERIMENTAL and subject to change without warning. It
* only works in Google Chrome.</b>
* only works in Google Chrome and Firefox >= 44.</b>
* @param {Element} remoteVideoElement a <code>&lt;video&gt;</code> DOM element
* to render video to.
* @param {Element} localVideoElement a <code>&lt;video&gt;</code> DOM element
@@ -125,7 +164,7 @@ MatrixCall.prototype.placeScreenSharingCall =
function(remoteVideoElement, localVideoElement) {
debuglog("placeScreenSharingCall");
checkForErrorListener(this);
const screenConstraints = _getChromeScreenSharingConstraints(this);
const screenConstraints = _getScreenSharingConstraints(this);
if (!screenConstraints) {
return;
}
@@ -207,22 +246,23 @@ MatrixCall.prototype.pauseElement = function(element, queueId) {
* serialising the operation into a chain of promises to avoid racing access
* to the element
* @param {Element} element HTMLMediaElement element to pause
* @param {string} src the src attribute value to assign to the element
* @param {MediaStream} srcObject the srcObject attribute value to assign to the element
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
*/
MatrixCall.prototype.assignElement = function(element, src, queueId) {
console.log("queuing assign on " + queueId + " element " + element + " for " + src);
MatrixCall.prototype.assignElement = function(element, srcObject, queueId) {
console.log("queuing assign on " + queueId + " element " + element + " for " +
srcObject);
if (this.mediaPromises[queueId]) {
this.mediaPromises[queueId] =
this.mediaPromises[queueId].then(function() {
console.log("previous promise completed for " + queueId);
element.src = src;
element.srcObject = srcObject;
}, function() {
console.log("previous promise failed for " + queueId);
element.src = src;
element.srcObject = srcObject;
});
} else {
element.src = src;
element.srcObject = srcObject;
}
};
@@ -262,9 +302,7 @@ MatrixCall.prototype.setLocalVideoElement = function(element) {
if (element && this.localAVStream && this.type === 'video') {
element.autoplay = true;
this.assignElement(element,
this.URL.createObjectURL(this.localAVStream),
"localVideo");
this.assignElement(element, this.localAVStream, "localVideo");
element.muted = true;
const self = this;
setTimeout(function() {
@@ -295,6 +333,7 @@ MatrixCall.prototype.setRemoteVideoElement = function(element) {
MatrixCall.prototype.setRemoteAudioElement = function(element) {
this.remoteVideoElement.muted = true;
this.remoteAudioElement = element;
this.remoteAudioElement.muted = false;
_tryPlayRemoteAudioStream(this);
};
@@ -366,15 +405,20 @@ MatrixCall.prototype.answer = function() {
debuglog("Answering call %s of type %s", this.callId, this.type);
const self = this;
if (self._answerContent) {
self._sendAnswer();
return;
}
if (!this.localAVStream && !this.waitForLocalAVStream) {
this.webRtc.getUserMedia(
_getUserMediaVideoContraints(this.type),
hookCallback(self, self._gotUserMediaForAnswer),
hookCallback(self, self._getUserMediaFailed),
hookCallback(self, self._maybeGotUserMediaForAnswer),
hookCallback(self, self._maybeGotUserMediaForAnswer),
);
setState(this, 'wait_local_media');
} else if (this.localAVStream) {
this._gotUserMediaForAnswer(this.localAVStream);
this._maybeGotUserMediaForAnswer(this.localAVStream);
} else if (this.waitForLocalAVStream) {
setState(this, 'wait_local_media');
}
@@ -393,11 +437,11 @@ MatrixCall.prototype._replacedBy = function(newCall) {
newCall.waitForLocalAVStream = true;
} else if (this.state == 'create_offer') {
debuglog("Handing local stream to new call");
newCall._gotUserMediaForAnswer(this.localAVStream);
newCall._maybeGotUserMediaForAnswer(this.localAVStream);
delete(this.localAVStream);
} else if (this.state == 'invite_sent') {
debuglog("Handing local stream to new call");
newCall._gotUserMediaForAnswer(this.localAVStream);
newCall._maybeGotUserMediaForAnswer(this.localAVStream);
delete(this.localAVStream);
}
newCall.localVideoElement = this.localVideoElement;
@@ -414,6 +458,8 @@ MatrixCall.prototype._replacedBy = function(newCall) {
* @param {boolean} suppressEvent True to suppress emitting an event.
*/
MatrixCall.prototype.hangup = function(reason, suppressEvent) {
if (this.state == 'ended') return;
debuglog("Ending call " + this.callId);
terminate(this, "local", reason, !suppressEvent);
const content = {
@@ -483,96 +529,144 @@ MatrixCall.prototype.isMicrophoneMuted = function() {
* @private
* @param {Object} stream
*/
MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
MatrixCall.prototype._maybeGotUserMediaForInvite = function(stream) {
if (this.successor) {
this.successor._gotUserMediaForAnswer(stream);
this.successor._maybeGotUserMediaForAnswer(stream);
return;
}
if (this.state == 'ended') {
return;
}
debuglog("_gotUserMediaForInvite -> " + this.type);
debuglog("_maybeGotUserMediaForInvite -> " + this.type);
const self = this;
const videoEl = this.getLocalVideoElement();
if (videoEl && this.type == 'video') {
videoEl.autoplay = true;
if (this.screenSharingStream) {
debuglog("Setting screen sharing stream to the local video element");
this.assignElement(videoEl,
this.URL.createObjectURL(this.screenSharingStream),
"localVideo");
} else {
this.assignElement(videoEl,
this.URL.createObjectURL(stream),
"localVideo");
}
videoEl.muted = true;
setTimeout(function() {
const vel = self.getLocalVideoElement();
if (vel.play) {
self.playElement(vel, "localVideo");
const error = stream;
const constraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': self.type === 'video',
},
};
if (stream instanceof MediaStream) {
const videoEl = this.getLocalVideoElement();
if (videoEl && this.type == 'video') {
videoEl.autoplay = true;
if (this.screenSharingStream) {
debuglog("Setting screen sharing stream to the local video" +
" element");
this.assignElement(videoEl, this.screenSharingStream, "localVideo");
} else {
this.assignElement(videoEl, stream, "localVideo");
}
}, 0);
videoEl.muted = true;
setTimeout(function() {
const vel = self.getLocalVideoElement();
if (vel.play) {
self.playElement(vel, "localVideo");
}
}, 0);
}
if (this.screenSharingStream) {
this.screenSharingStream.addTrack(stream.getAudioTracks()[0]);
stream = this.screenSharingStream;
}
this.localAVStream = stream;
// why do we enable audio (and only audio) tracks here? -- matthew
setTracksEnabled(stream.getAudioTracks(), true);
this.peerConn = _createPeerConnection(this);
this.peerConn.addStream(stream);
} else if (error.name === 'PermissionDeniedError') {
debuglog('User denied access to camera/microphone.' +
' Or possibly you are using an insecure domain. Receiving only.');
this.peerConn = _createPeerConnection(this);
} else {
debuglog('Failed to getUserMedia.');
this._getUserMediaFailed(error);
return;
}
if (this.screenSharingStream) {
this.screenSharingStream.addTrack(stream.getAudioTracks()[0]);
stream = this.screenSharingStream;
}
this.localAVStream = stream;
// why do we enable audio (and only audio) tracks here? -- matthew
setTracksEnabled(stream.getAudioTracks(), true);
this.peerConn = _createPeerConnection(this);
this.peerConn.addStream(stream);
this.peerConn.createOffer(
hookCallback(self, self._gotLocalOffer),
hookCallback(self, self._getLocalOfferFailed),
constraints,
);
setState(self, 'create_offer');
};
MatrixCall.prototype._sendAnswer = function(stream) {
sendEvent(this, 'm.call.answer', this._answerContent).then(() => {
setState(this, 'connecting');
// If this isn't the first time we've tried to send the answer,
// we may have candidates queued up, so send them now.
_sendCandidateQueue(this);
}).catch((error) => {
// We've failed to answer: back to the ringing state
setState(this, 'ringing');
this.client.cancelPendingEvent(error.event);
let code = MatrixCall.ERR_SEND_ANSWER;
let message = "Failed to send answer";
if (error.name == 'UnknownDeviceError') {
code = MatrixCall.ERR_UNKNOWN_DEVICES;
message = "Unknown devices present in the room";
}
this.emit("error", callError(code, message));
throw error;
});
};
/**
* Internal
* @private
* @param {Object} stream
*/
MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
MatrixCall.prototype._maybeGotUserMediaForAnswer = function(stream) {
const self = this;
if (self.state == 'ended') {
return;
}
const localVidEl = self.getLocalVideoElement();
if (localVidEl && self.type == 'video') {
localVidEl.autoplay = true;
this.assignElement(localVidEl,
this.URL.createObjectURL(stream),
"localVideo");
localVidEl.muted = true;
setTimeout(function() {
const vel = self.getLocalVideoElement();
if (vel.play) {
self.playElement(vel, "localVideo");
}
}, 0);
const error = stream;
if (stream instanceof MediaStream) {
const localVidEl = self.getLocalVideoElement();
if (localVidEl && self.type == 'video') {
localVidEl.autoplay = true;
this.assignElement(localVidEl, stream, "localVideo");
localVidEl.muted = true;
setTimeout(function() {
const vel = self.getLocalVideoElement();
if (vel.play) {
self.playElement(vel, "localVideo");
}
}, 0);
}
self.localAVStream = stream;
setTracksEnabled(stream.getAudioTracks(), true);
self.peerConn.addStream(stream);
} else if (error.name === 'PermissionDeniedError') {
debuglog('User denied access to camera/microphone.' +
' Or possibly you are using an insecure domain. Receiving only.');
} else {
debuglog('Failed to getUserMedia.');
this._getUserMediaFailed(error);
return;
}
self.localAVStream = stream;
setTracksEnabled(stream.getAudioTracks(), true);
self.peerConn.addStream(stream);
const constraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': self.type == 'video',
'OfferToReceiveVideo': self.type === 'video',
},
};
self.peerConn.createAnswer(function(description) {
debuglog("Created answer: " + description);
self.peerConn.setLocalDescription(description, function() {
const content = {
self._answerContent = {
version: 0,
call_id: self.callId,
answer: {
@@ -580,8 +674,7 @@ MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
type: self.peerConn.localDescription.type,
},
};
sendEvent(self, 'm.call.answer', content);
setState(self, 'connecting');
self._sendAnswer();
}, function() {
debuglog("Error setting local description!");
}, constraints);
@@ -602,6 +695,9 @@ MatrixCall.prototype._gotLocalIceCandidate = function(event) {
"Got local ICE " + event.candidate.sdpMid + " candidate: " +
event.candidate.candidate,
);
if (this.state == 'ended') return;
// As with the offer, note we need to make a copy of this object, not
// pass the original: that broke in Chrome ~m43.
const c = {
@@ -684,14 +780,26 @@ MatrixCall.prototype._gotLocalOffer = function(description) {
},
lifetime: MatrixCall.CALL_TIMEOUT_MS,
};
sendEvent(self, 'm.call.invite', content);
setTimeout(function() {
if (self.state == 'invite_sent') {
self.hangup('invite_timeout');
sendEvent(self, 'm.call.invite', content).then(() => {
setState(self, 'invite_sent');
setTimeout(function() {
if (self.state == 'invite_sent') {
self.hangup('invite_timeout');
}
}, MatrixCall.CALL_TIMEOUT_MS);
}).catch((error) => {
let code = MatrixCall.ERR_SEND_INVITE;
let message = "Failed to send invite";
if (error.name == 'UnknownDeviceError') {
code = MatrixCall.ERR_UNKNOWN_DEVICES;
message = "Unknown devices present in the room";
}
}, MatrixCall.CALL_TIMEOUT_MS);
setState(self, 'invite_sent');
self.client.cancelPendingEvent(error.event);
terminate(self, "local", code, false);
self.emit("error", callError(code, message));
throw error;
});
}, function() {
debuglog("Error setting local description!");
});
@@ -715,6 +823,7 @@ MatrixCall.prototype._getLocalOfferFailed = function(error) {
* @param {Object} error
*/
MatrixCall.prototype._getUserMediaFailed = function(error) {
terminate(this, "local", 'user_media_failed', false);
this.emit(
"error",
callError(
@@ -723,7 +832,6 @@ MatrixCall.prototype._getUserMediaFailed = function(error) {
"does this app have permission?",
),
);
this.hangup("user_media_failed");
};
/**
@@ -910,6 +1018,13 @@ const sendCandidate = function(self, content) {
// Sends candidates with are sent in a special way because we try to amalgamate
// them into one message
self.candidateSendQueue.push(content);
// Don't send the ICE candidates yet if the call is in the ringing state: this
// means we tried to pick (ie. started generating candidates) and then failed to
// send the answer and went back to the ringing state. Queue up the candidates
// to send if we sucessfully send the answer.
if (self.state == 'ringing') return;
if (self.candidateSendTries === 0) {
setTimeout(function() {
_sendCandidateQueue(self);
@@ -922,19 +1037,19 @@ const terminate = function(self, hangupParty, hangupReason, shouldEmit) {
if (self.getRemoteVideoElement().pause) {
self.pauseElement(self.getRemoteVideoElement(), "remoteVideo");
}
self.assignElement(self.getRemoteVideoElement(), "", "remoteVideo");
self.assignElement(self.getRemoteVideoElement(), null, "remoteVideo");
}
if (self.getRemoteAudioElement()) {
if (self.getRemoteAudioElement().pause) {
self.pauseElement(self.getRemoteAudioElement(), "remoteAudio");
}
self.assignElement(self.getRemoteAudioElement(), "", "remoteAudio");
self.assignElement(self.getRemoteAudioElement(), null, "remoteAudio");
}
if (self.getLocalVideoElement()) {
if (self.getLocalVideoElement().pause) {
self.pauseElement(self.getLocalVideoElement(), "localVideo");
}
self.assignElement(self.getLocalVideoElement(), "", "localVideo");
self.assignElement(self.getLocalVideoElement(), null, "localVideo");
}
self.hangupParty = hangupParty;
self.hangupReason = hangupReason;
@@ -992,9 +1107,7 @@ const _tryPlayRemoteStream = function(self) {
if (self.getRemoteVideoElement() && self.remoteAVStream) {
const player = self.getRemoteVideoElement();
player.autoplay = true;
self.assignElement(player,
self.URL.createObjectURL(self.remoteAVStream),
"remoteVideo");
self.assignElement(player, self.remoteAVStream, "remoteVideo");
setTimeout(function() {
const vel = self.getRemoteVideoElement();
if (vel.play) {
@@ -1008,13 +1121,15 @@ const _tryPlayRemoteStream = function(self) {
}
};
const _tryPlayRemoteAudioStream = function(self) {
const _tryPlayRemoteAudioStream = async function(self) {
if (self.getRemoteAudioElement() && self.remoteAStream) {
const player = self.getRemoteAudioElement();
// if audioOutput is non-default:
if (audioOutput) await player.setSinkId(audioOutput);
player.autoplay = true;
self.assignElement(player,
self.URL.createObjectURL(self.remoteAStream),
"remoteAudio");
self.assignElement(player, self.remoteAStream, "remoteAudio");
setTimeout(function() {
const ael = self.getRemoteAudioElement();
if (ael.play) {
@@ -1092,8 +1207,8 @@ const _placeCallWithConstraints = function(self, constraints) {
self.client.callList[self.callId] = self;
self.webRtc.getUserMedia(
constraints,
hookCallback(self, self._gotUserMediaForInvite),
hookCallback(self, self._getUserMediaFailed),
hookCallback(self, self._maybeGotUserMediaForInvite),
hookCallback(self, self._maybeGotUserMediaForInvite),
);
setState(self, 'wait_local_media');
self.direction = 'outbound';
@@ -1117,6 +1232,7 @@ const _createPeerConnection = function(self) {
}
const pc = new self.webRtc.RtcPeerConnection({
iceTransportPolicy: self.forceTURN ? 'relay' : undefined,
iceServers: servers,
});
pc.oniceconnectionstatechange = hookCallback(self, self._onIceConnectionStateChanged);
@@ -1126,7 +1242,7 @@ const _createPeerConnection = function(self) {
return pc;
};
const _getChromeScreenSharingConstraints = function(call) {
const _getScreenSharingConstraints = function(call) {
const screen = global.screen;
if (!screen) {
call.emit("error", callError(
@@ -1138,6 +1254,7 @@ const _getChromeScreenSharingConstraints = function(call) {
return {
video: {
mediaSource: 'screen',
mandatory: {
chromeMediaSource: "screen",
chromeMediaSourceId: "" + Date.now(),
@@ -1151,18 +1268,28 @@ const _getChromeScreenSharingConstraints = function(call) {
};
const _getUserMediaVideoContraints = function(callType) {
const isWebkit = !!global.window.navigator.webkitGetUserMedia;
switch (callType) {
case 'voice':
return ({audio: true, video: false});
return {
audio: {
deviceId: audioInput ? {exact: audioInput} : undefined,
}, video: false,
};
case 'video':
return ({audio: true, video: {
mandatory: {
minWidth: 640,
maxWidth: 640,
minHeight: 360,
maxHeight: 360,
return {
audio: {
deviceId: audioInput ? {exact: audioInput} : undefined,
}, video: {
deviceId: videoInput ? {exact: videoInput} : undefined,
/* We want 640x360. Chrome will give it only if we ask exactly,
FF refuses entirely if we ask exactly, so have to ask for ideal
instead */
width: isWebkit ? { exact: 640 } : { ideal: 640 },
height: isWebkit ? { exact: 360 } : { ideal: 360 },
},
}});
};
}
};
@@ -1194,14 +1321,40 @@ const forAllTracksOnStream = function(s, f) {
/** The MatrixCall class. */
module.exports.MatrixCall = MatrixCall;
let audioOutput;
let audioInput;
let videoInput;
/**
* Set an audio output device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setAudioOutput = function(deviceId) { audioOutput = deviceId; };
/**
* Set an audio input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setAudioInput = function(deviceId) { audioInput = deviceId; };
/**
* Set a video input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setVideoInput = function(deviceId) { videoInput = deviceId; };
/**
* Create a new Matrix call for the browser.
* @param {MatrixClient} client The client instance to use.
* @param {string} roomId The room the call is in.
* @param {Object?} options DEPRECATED optional options map.
* @param {boolean} options.forceTURN DEPRECATED whether relay through TURN should be forced. This option is deprecated - use opts.forceTURN when creating the matrix client since it's only possible to set this option on outbound calls.
* @return {MatrixCall} the call or null if the browser doesn't support calling.
*/
module.exports.createNewMatrixCall = function(client, roomId) {
module.exports.createNewMatrixCall = function(client, roomId, options) {
const w = global.window;
const doc = global.document;
if (!w || !doc) {
@@ -1251,12 +1404,17 @@ module.exports.createNewMatrixCall = function(client, roomId) {
!webRtc.RtcPeerConnection || !webRtc.getUserMedia) {
return null; // WebRTC is not supported.
}
const optionsForceTURN = options ? options.forceTURN : false;
const opts = {
webRtc: webRtc,
client: client,
URL: w.URL,
roomId: roomId,
turnServers: client.getTurnServers(),
// call level options
forceTURN: client._forceTURN || optionsForceTURN,
};
return new MatrixCall(opts);
};
Executable
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
set -ex
npm run lint
# install Olm so that we can run the crypto tests.
npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz
npm run test
npm run gendoc