Compare commits

..

181 Commits

Author SHA1 Message Date
Kegan Dougal a750a4756f Merge branch 'develop' 2015-07-28 13:19:51 +01:00
Kegan Dougal fed91a5090 Add 0.2.0 to /dist 2015-07-28 13:19:34 +01:00
Kegan Dougal 3e1940fe17 Merge branch 'develop' 2015-07-28 13:18:29 +01:00
Kegan Dougal bfefa380ae Update README; v0.2.0 release commit. 2015-07-28 13:17:05 +01:00
Kegan Dougal 14e91624bd no-op if joinRoom is called when you're already joined. Add test for this.
Also ignore missing require()s when building (missing olm).
2015-07-28 13:13:55 +01:00
Kegan Dougal 551721b22e More WIP docs 2015-07-28 11:53:07 +01:00
Kegan Dougal 1ef7af258d Fix linting. Mark on new Store classes as WIP. 2015-07-28 11:50:47 +01:00
Kegan Dougal 3905e56e1f Bump to 0.2.0 - Add missing jsdoc, update CHANGELOG. 2015-07-28 11:45:46 +01:00
Kegan Dougal 9993e5dbed Cleanup jsdoc. Allow extra query params to be set on requests (for ASes). 2015-07-28 10:57:05 +01:00
Matthew Hodgson 14a7ff3e04 differentiate errors a bit better 2015-07-23 11:01:43 -07:00
Mark Haines 0737b80850 Check thet Olm.Account and Olm.Session are defined before enabling crypto 2015-07-23 10:19:41 +01:00
Mark Haines 19b4110a51 Merge pull request #18 from matrix-org/disable_end_to_end
Disable end-to-end encryption if olm isn't installed.
2015-07-23 10:09:35 +01:00
Mark Haines ea1c315aa8 Enable crypto if olm is present 2015-07-23 10:03:20 +01:00
Mark Haines 58e44a2fc3 Disable end-to-end crypto 2015-07-23 09:30:50 +01:00
Mark Haines ae8e01839e Add flag for indicating if an event was encrypted 2015-07-22 11:57:34 +01:00
Mark Haines 9e5f30fe1f Fix some lint errors 2015-07-22 11:37:22 +01:00
Mark Haines 319ca5123e Return device key list in a stable order, add a check for the list in the tests 2015-07-22 11:34:38 +01:00
Mark Haines 28a5719f5f Add method for listing the device keys we have for a user 2015-07-22 11:24:58 +01:00
David Baker 348d3f4ce1 Merge pull request #17 from matrix-org/markjh/end_to_end
Add support for end-to-end using olm
2015-07-21 13:12:01 -07:00
David Baker a0fc8df3e1 Fix more minor things from PR comments 2015-07-21 13:10:01 -07:00
David Baker 8b2f11edc9 s/.then/.done/ 2015-07-21 13:08:13 -07:00
Mark Haines 09f7011bc5 Add method for telling if room is encrypted 2015-07-21 18:09:10 +01:00
Mark Haines ffbc197650 Flush the httpBackend in the last crypto test 2015-07-21 17:53:04 +01:00
Mark Haines b33dcec74b Dummy commit to poke jenkins 2015-07-21 17:47:01 +01:00
Mark Haines f079e021a2 Line too long 2015-07-21 17:41:05 +01:00
Mark Haines 02c2e88a3e Fix some of the lint warnings 2015-07-21 17:39:11 +01:00
Kegan Dougal 358cdaf167 Fix linting errors 2015-07-21 17:20:35 +01:00
Kegan Dougal 90e778cb84 Handle redactions (both live and historic). 2015-07-21 17:14:11 +01:00
Mark Haines 816bb61689 Increase the timeout for the last crypto integ test 2015-07-21 17:07:10 +01:00
Kegan Dougal 6653d294d9 Re-apply metadata after setStateEvents to correctly apply sender/target props for m.room.member events. 2015-07-21 16:58:14 +01:00
Mark Haines 9fe6d9b782 Add method for disabling end-to-end for a room 2015-07-21 16:44:47 +01:00
Kegsay 185cbac85c Merge pull request #16 from matrix-org/getMembersWithMembership
Add getMembersWithMembership to get all members with given membership
2015-07-21 15:42:00 +01:00
Erik Johnston f716431f65 Make getJoinedUsers call getMembersWithMembership 2015-07-21 15:41:33 +01:00
Erik Johnston 86e73bdd35 Add getMembersWithMembership to get all members with given membership state 2015-07-21 15:31:58 +01:00
Mark Haines 67318247c8 Use an npmified version of olm.js 2015-07-21 15:09:54 +01:00
Mark Haines 3be472330e Update olm.js 2015-07-21 14:30:54 +01:00
Mark Haines 6dae0ef327 Fix the local echo for encrypted events by sending a single event with different content and event type for the wire vs local display 2015-07-21 14:19:24 +01:00
Mark Haines 101de29f3e Store sessions under the right key after encrypting, set the room_id key in the encrypted payload 2015-07-21 11:59:46 +01:00
Mark Haines b5d8bc5c5f Add the timestamp to decrypted events, update the copy of olm.js 2015-07-21 11:48:30 +01:00
Mark Haines a97b11136a Update olm.js 2015-07-20 16:36:45 +01:00
Kegan Dougal 93a104fe4c Fire presence first time if presence event unset. 2015-07-20 11:28:13 +01:00
Mark Haines 00ae144997 Don't try to decrypt end-to-end messages if the client doesn't have end-to-end support enabled 2015-07-20 10:58:20 +01:00
Mark Haines 65d64bf223 Merge remote-tracking branch 'origin/develop' into markjh/end_to_end
Conflicts:
	lib/client.js
	lib/matrix.js
2015-07-20 10:43:23 +01:00
Mark Haines d20bd41ff9 Update olm.js 2015-07-20 10:41:54 +01:00
Mark Haines a375aa30ea Add script for running tests without converage 2015-07-20 10:36:30 +01:00
Kegan Dougal 32c4d3ffc4 Parse m.presence events from /events 2015-07-20 10:07:34 +01:00
Matthew Hodgson 8bc89d433b fix non-impacting typo 2015-07-19 00:36:56 +01:00
Matthew Hodgson bc216d7bca fix stale video from appearing unexpectedly when you place a new call 2015-07-18 23:29:08 +01:00
Matthew Hodgson beb8bff084 placeholder code for defaulting displaynames to userids, except it needs disambiguation logic 2015-07-18 15:40:19 +01:00
Mark Haines 110546bfca Add a copy of olm.js so that branch will work as temporary work around until I work out how to package it using npm 2015-07-17 19:22:21 +01:00
Mark Haines 62ac369500 Fix test name 2015-07-17 18:51:23 +01:00
Mark Haines 8e6e1e64c7 Add integration test for the end-to-end crypto 2015-07-17 18:47:34 +01:00
Matthew Hodgson 9d5fc7c0fd at long last fix bug where /op would cache previous failed attempts. fixes SYWEB-342 2015-07-17 18:02:06 +01:00
David Baker c563380a0b Fall back to the identicon generator if a user doesn't have an avatar_url 2015-07-17 17:48:57 +01:00
Kegan Dougal 7869d7a7df Emit state changes after updating hangup party and reason 2015-07-17 17:03:18 +01:00
Kegan Dougal 7ec42f8843 Firefox falls over in a heap if you don't provide an error callback. 2015-07-17 16:15:08 +01:00
Kegan Dougal 3d6b9f4593 Add more logging when sync requests fail. 2015-07-17 14:46:52 +01:00
Kegan Dougal 4489c85020 Play video to local video elements when updated. 2015-07-17 14:23:40 +01:00
Kegan Dougal a694da344c Remove unecessary logging. 2015-07-17 13:57:39 +01:00
Kegan Dougal 384fb94262 Linting 2015-07-17 13:39:04 +01:00
Kegan Dougal 8727dd1e34 Use TURN servers when placing/receiving calls. 2015-07-17 13:36:58 +01:00
Kegan Dougal 1232f6ce3c Periodically check turn servers. 2015-07-17 12:23:39 +01:00
Kegsay 1888c83f4f Merge pull request #15 from EricssonResearch/msba/saml2
Add SAML2 login method helper
2015-07-17 08:37:45 +01:00
Mark Haines 2ee5977ad2 Start integrating end-to-end into the matrix-client. Add a storage class to store end-to-end sessions. Implement the one-time key upload API, and start sketching out the encryption and decryption functions 2015-07-16 18:21:25 +01:00
Kegan Dougal 103869e628 Accept MatrixEvents like everywhere else in the SDK. 2015-07-16 17:15:40 +01:00
Kegan Dougal 6bd3b0db0d Emit state changes from MatrixCall. 2015-07-16 11:36:35 +01:00
Kegsay c3696d07b7 Merge pull request #14 from matrix-org/registration
Registration
2015-07-16 10:22:11 +01:00
David Baker 7b26a64617 camels 2015-07-16 10:20:31 +01:00
David Baker ff1b137750 add js doc for httpStatus 2015-07-16 10:18:51 +01:00
Kegan Dougal c5718891c5 Fix tests. Add MatrixCall.setLocalVideoElement 2015-07-16 10:07:19 +01:00
Kegsay 4b4f13d662 Merge pull request #13 from matrix-org/set_password
Implement setPassword API
2015-07-16 09:59:32 +01:00
Erik Johnston ebb21f56d3 No trailing commas :( 2015-07-16 09:58:18 +01:00
Erik Johnston ec5352183a Camel case 2015-07-16 09:56:36 +01:00
Erik Johnston 83563a23fe Implement setPassword API 2015-07-16 09:50:12 +01:00
Matthew Hodgson 260d9abced fix more NPEs 2015-07-16 01:37:39 +01:00
Matthew Hodgson ee4d0fb9ee hopefully fix NPE that bit Antony 2015-07-16 01:21:29 +01:00
David Baker 13f72e1df2 lint 2015-07-15 19:26:52 +01:00
David Baker a89de9754f Support registration & next_links 2015-07-15 19:25:08 +01:00
David Baker b2f5d0b40a Merge pull request #12 from matrix-org/voip
WebRTC support
2015-07-15 10:20:56 +01:00
David Baker 9f3f33e2cc s/Safari/OpenWebRTC/ 2015-07-15 10:16:43 +01:00
Kegan Dougal 309b61586f Linting. 2015-07-15 09:48:37 +01:00
Kegsay be1264d5c3 Update README.md 2015-07-15 09:47:25 +01:00
Matthew Hodgson 3a344a0138 turn off alias disambiguation, and add a method to grab room avatar urls 2015-07-15 03:53:51 +01:00
David Baker ae7f9ab871 lint 2015-07-14 19:38:56 +01:00
David Baker 77d3add846 Talk to the ID server 2015-07-14 19:36:44 +01:00
Kegan Dougal 4cf0e10c02 Bug fixes. Everything should be working now. 2015-07-14 17:55:55 +01:00
Kegan Dougal e3fdcaaff5 Add noddy voip example app. 2015-07-14 17:11:30 +01:00
Kegan Dougal 053a5b1bea Make inbound calls work. 2015-07-14 16:23:31 +01:00
Kegan Dougal 8a41504cbb Glue in call handling into MatrixClient. Outbound calls work. 2015-07-14 16:06:22 +01:00
Kegan Dougal 8af5ed363b Merge branch 'develop' into voip 2015-07-14 12:04:00 +01:00
Kegan Dougal 8cb973e605 Fix SYJS-16: event.sender is sometimes null for locally sent events. 2015-07-14 12:00:19 +01:00
Kegan Dougal 3e60842c3b Sort out access levels for functions; add JSDoc. 2015-07-14 11:11:41 +01:00
Kegan Dougal 7cce41df2e Add structured errors and callbacks. Now sends candidates. 2015-07-14 10:48:19 +01:00
David Baker 58df45814b lint errors 2015-07-14 10:09:08 +01:00
David Baker 864fdcb925 Support V2 registration 2015-07-13 19:15:10 +01:00
Kegan Dougal 0ef20faff7 Add JSDoc; Add createNewMatrixCall to globals. 2015-07-13 17:11:37 +01:00
Kegan Dougal 3bd043a8eb Add MatrixCall class; ported from angular. Untested and probably broken. 2015-07-13 16:45:31 +01:00
David Baker 9f930f7d0d Redundant suffix is redundant. 2015-07-13 14:39:11 +01:00
Kegan Dougal c289c70f27 SYJS-17: Update the core event dict completely before members.
The RoomState needs to be current before higher-level processing as this
processing may depend on the end state rather than the progressive state
(e.g. disambiguating BOTH RoomMember's display names).
2015-07-13 10:53:07 +01:00
David Baker a6b0ea8825 Spaces around operators 2015-07-08 16:13:54 +01:00
David Baker 34086f48fa Separate impl for node and extend example node app to use it. 2015-07-08 16:13:10 +01:00
David Baker 5e0d5a5b05 Fix some linitng errors 2015-07-08 15:01:38 +01:00
Matthew Hodgson f75453d5d5 ignore parted users when naming rooms 2015-07-08 14:55:08 +01:00
David Baker 639522f80d Add uploadContent method 2015-07-08 14:33:46 +01:00
David Baker e2768ceba2 add mxc -> http uri conversion func to client 2015-07-07 13:39:37 +01:00
Muthu Subramanian b3726411f4 Add SAML2 login method helper 2015-07-07 17:50:11 +05:30
Kegsay e706dcd861 Merge pull request #11 from matrix-org/avatar_url
Move getAvatarUrl method to client so it can do mxc -> http uri conversion
2015-07-02 21:15:37 +01:00
David Baker 04272a66f0 Add jsdoc and pass through other params 2015-07-02 18:29:35 +01:00
David Baker f29f2a8115 Move getAvatarUrl into the client because it requires an instantiated client to work. Fix bits of client to use opts rather than credentials which doesn't exist. 2015-07-02 18:22:46 +01:00
Kegan Dougal 5691865035 Linting 2015-07-02 17:23:35 +01:00
Kegan Dougal abe00deb65 Add getAvatarUrl() to RoomMember 2015-07-02 17:22:44 +01:00
Kegan Dougal 4c48990c1f Decorate setItem/getItem to map between Objects and Strings. 2015-07-02 13:27:13 +01:00
Kegan Dougal df8ec50631 Add storeEvents UTs; fix bugs as a result. 2015-07-02 11:37:50 +01:00
Kegan Dougal ea738e31ba Add storeEvents impl. 2015-07-02 11:03:50 +01:00
Kegan Dougal 023a3cf2bd Finish scrollback UTs. 2015-07-02 10:22:53 +01:00
Kegan Dougal 23c3ce92d7 Finish scrollback impl. Add UTs. 2015-07-02 10:13:51 +01:00
Kegsay df7bde4257 Merge pull request #10 from matrix-org/push
Push
2015-07-01 18:19:43 +01:00
Kegan Dougal 82de3b6f19 Begin scrollback implementation.
Add Room.storageToken to docs. Add unit test stubs for scrollback function.
Add a basic common-case implementation.
2015-07-01 18:04:59 +01:00
David Baker 214697eec9 Fix long line 2015-07-01 18:03:29 +01:00
David Baker db790f0b19 fixjsstyle 2015-07-01 18:02:35 +01:00
David Baker c81cb1fcfd Tests for pushprocessor 2015-07-01 18:01:48 +01:00
Kegan Dougal a890bff30f Actually flip the cache bit 2015-07-01 16:39:32 +01:00
Kegan Dougal 9fd847567c Add get/setUser and getRooms with UTs. 2015-07-01 16:38:13 +01:00
Kegan Dougal 213caf7c26 Add old state check. 2015-07-01 15:33:58 +01:00
Kegan Dougal 732f9dc592 Restructure timeline batches to have high numbers = newest; it's less confusing that way. 2015-07-01 15:28:41 +01:00
Kegan Dougal 22506513b4 Add more webstore unit tests. 2015-07-01 14:20:14 +01:00
David Baker 753a11ab7f Move push actions method to the client 2015-07-01 14:09:34 +01:00
Kegan Dougal 994fcceace Add another storeRoom test. Add stub tests for WebStorage. 2015-07-01 12:03:34 +01:00
Kegan Dougal 2d08a6530c Don't need extra escaping; doesn't take coverage correctly. 2015-07-01 11:41:55 +01:00
Kegan Dougal 841e02d102 Don't minify tests so we can get proper stack traces when running 'npm test'. 2015-07-01 10:33:03 +01:00
Kegan Dougal a0fddf7ab3 Linting. 2015-06-30 17:58:35 +01:00
Kegan Dougal c8da373ecc Add first cut webstorage implementation. Add very basic test. 2015-06-30 17:56:58 +01:00
Kegan Dougal 3a8d99496c Merge branch 'master' into develop 2015-06-30 13:46:48 +01:00
Kegsay 319595407c Tweak example to mention startClient() 2015-06-30 13:46:27 +01:00
Kegan Dougal 388cd3a6da Add init/del structs 2015-06-30 12:00:58 +01:00
Kegan Dougal aaecda53d6 Add scrollback to store interface 2015-06-30 11:48:49 +01:00
Kegan Dougal 5d5d76d154 Pass a store implementation rather than kind string.
Makes it easier to test.
2015-06-30 11:39:57 +01:00
Kegan Dougal 796afb104b s/localstorage/webstorage/g. Add storage impl notes.
WebStorage is preferred since we can trivially support local or session storage.
Add design notes for persisting rooms.
2015-06-30 11:30:49 +01:00
David Baker 9416a9a8a3 Fix linting & c+p errors 2015-06-29 18:20:36 +01:00
David Baker 22f08e4e5b Get rid of micromatch & reinstate our hacky version because micromatch, ironically, is huge. 2015-06-29 18:16:49 +01:00
Kegan Dougal a476b11f0c Add local storage impl notes 2015-06-29 17:55:11 +01:00
David Baker c82b2049eb prefix internal var with underscrore 2015-06-29 16:34:33 +01:00
David Baker 2ab8f8cf76 closure linting errors 2015-06-29 16:21:32 +01:00
David Baker 53f2f156ca Fix linting errors 2015-06-29 16:09:07 +01:00
David Baker 083a0980eb javascript function syntax #3... 2015-06-29 15:57:20 +01:00
David Baker 1be02a3c3c Port over push rule eveluator from the angular SDK and make it available on MatrixEvent 2015-06-29 15:53:23 +01:00
David Baker 21150e7587 Doc fail 2015-06-26 16:50:55 +01:00
David Baker 3806c4d566 Missing semicolon 2015-06-26 16:49:07 +01:00
David Baker b814872c37 unnecessary semicolon 2015-06-26 16:45:59 +01:00
David Baker b2656024bd Merge branch 'develop' into push
Conflicts:
	lib/client.js
2015-06-26 16:39:32 +01:00
Kegan Dougal b7975866fa Add getSyncToken and setSyncToken to data store interface.
This allows local storage to hold onto the token across page refreshes.
2015-06-26 15:36:53 +01:00
Kegan Dougal 6078100465 Export LocalStorageStore class. Throw if 'localStorage' isn't defined. 2015-06-26 15:24:55 +01:00
Kegan Dougal a3a59f4456 Add localstorage store module. 2015-06-26 15:13:04 +01:00
David Baker 9ea080b7bb Fetch the user's push rules before any events arrive, so we can annotate events with push data when they arrive. 2015-06-26 15:11:27 +01:00
Kegan Dougal 20b5553e48 Implement MatrixClient tests without a scheduler/store. 2015-06-26 14:26:57 +01:00
Kegan Dougal fd0eb0c674 Finish timline integration tests. 2015-06-26 13:53:01 +01:00
Kegan Dougal 3c4bda8580 Add MatrixScheduler.removeEventFromQueue/getQueueForEvent and QUEUED state.
This is to allow the UI to show "queued" on events as well as allow the
removal of events in the queue.
2015-06-26 09:52:52 +01:00
Kegan Dougal 320bbec737 Add MatrixScheduler.getQueue to allow items in the queue to be modified. 2015-06-25 18:00:07 +01:00
Kegan Dougal 041f96ae6d Add remaining stub tests 2015-06-25 17:56:18 +01:00
Kegan Dougal f240563fb5 Add room state/syncing stub tests 2015-06-25 17:49:46 +01:00
Kegan Dougal e8d17a5ab3 Add missing pagination test 2015-06-25 17:36:21 +01:00
Kegan Dougal f49234a772 Add pagination tests 2015-06-25 17:29:25 +01:00
Kegan Dougal 46d5c2bd61 Add pagination test; fix null token bug. 2015-06-25 16:33:22 +01:00
Kegan Dougal 325c00c660 Set Room.oldState.paginationToken to null at the start of the timeline. Fix linting errors. 2015-06-25 15:28:25 +01:00
Kegan Dougal c4c68a0287 Add local echo tests. 2015-06-25 15:24:56 +01:00
Kegan Dougal 21df000604 Add Room.addEvents UT 2015-06-25 14:00:52 +01:00
Kegan Dougal 0be3c6837d Add utils UTs. Remove unused functions. 2015-06-24 17:42:02 +01:00
Kegan Dougal 74fcbdee3f Add retryFn -1 test. 2015-06-24 17:14:11 +01:00
Kegan Dougal 332bdcd101 Add Scheduler UTs. 2015-06-24 17:05:06 +01:00
Kegan Dougal 913178b284 Linting 2015-06-24 15:38:18 +01:00
Kegan Dougal 2a83248168 Add User UTs 2015-06-24 15:37:55 +01:00
Kegan Dougal 8f16948430 Appease linters 2015-06-24 14:57:48 +01:00
Kegan Dougal 1b665c176a Add RoomState UTs 2015-06-24 14:57:04 +01:00
Kegan Dougal 0bc9951f13 Add RoomState UT stubs. 2015-06-24 13:52:17 +01:00
Kegan Dougal d3377e3dea Add RoomMember UTs. Fix disambiguation bug. Add CHANGELOG. 2015-06-24 13:35:19 +01:00
Kegan Dougal b3efafebbe Restructure test layout. 2015-06-24 12:53:15 +01:00
Kegan Dougal a12133ec9f Appease linters 2015-06-24 11:44:00 +01:00
Kegan Dougal f763eb3f37 Convert all helper functions which make events to use kwargs 2015-06-24 11:43:16 +01:00
Kegan Dougal 26a23b19b3 Use an opts for helper mk functions; poor man's keyword args. 2015-06-24 11:14:54 +01:00
Kegan Dougal 9d967fb232 Add some UTs to Room methods 2015-06-24 11:05:18 +01:00
Kegan Dougal 62d7ff5ba9 Add colours to terminal. Sort room list by TS. 2015-06-23 17:46:52 +01:00
44 changed files with 17416 additions and 498 deletions
+2
View File
@@ -0,0 +1,2 @@
instrumentation:
compact: false
+66
View File
@@ -1,3 +1,69 @@
Changes in 0.2.0
================
Breaking changes:
* `MatrixClient.setPowerLevel` now expects a `MatrixEvent` and not an `Object`
for the `event` parameter.
New features:
* Added `EventStatus.QUEUED` which is set on an event when it is waiting to be
sent by the scheduler and there are other events in front.
* Added support for processing push rules on an event. This can be obtained by
calling `MatrixClient.getPushActionsForEvent(event)`.
* Added WebRTC support. Outbound calls can be made via
`call = global.createNewMatrixCall(MatrixClient, roomId)` followed by
`call.placeVoiceCall()` or `call.placeVideoCall(remoteEle, localEle)`.
Inbound calls will be received via the event `"Call.incoming"` which provides
a call object which can be followed with `call.answer()` or `call.hangup()`.
* Added the ability to upload files to the media repository.
* Added the ability to change the client's password.
* Added the ability to register with an email via an identity server.
* Handle presence events by updating the associated `User` object.
* Handle redaction events.
* Added infrastructure for supporting End-to-End encryption. E2E is *NOT*
available in this version.
New methods:
* `MatrixClient.getUser(userId)`
* `MatrixClient.getPushActionsForEvent(event)`
* `MatrixClient.setPassword(auth, newPassword)`
* `MatrixClient.loginWithSAML2(relayState, callback)`
* `MatrixClient.getAvatarUrlForMember(member, w, h, method)`
* `MatrixClient.mxcUrlToHttp(url, w, h, method)`
* `MatrixClient.getAvatarUrlForRoom(room, w, h, method)`
* `MatrixClient.uploadContent(file, callback)`
* `Room.getMembersWithMemership(membership)`
* `MatrixScheduler.getQueueForEvent(event)`
* `MatrixScheduler.removeEventFromQueue(event)`
* `$DATA_STORE.setSyncToken(token)`
* `$DATA_STORE.getSyncToken()`
Crypto infrastructure (crypto is *NOT* available in this version):
* `global.CRYPTO_ENABLED`
* `MatrixClient.isCryptoEnabled()`
* `MatrixClient.uploadKeys(maxKeys)`
* `MatrixClient.downloadKeys(userIds, forceDownload)`
* `MatrixClient.listDeviceKeys(userId)`
* `MatrixClient.setRoomEncryption(roomId, config)`
* `MatrixClient.isRoomEncrypted(roomId)`
New classes:
* `MatrixCall`
* `WebStorageStore` - *WIP; unstable*
* `WebStorageSessionStore` - *WIP; unstable*
Bug fixes:
* Member name bugfix: Fixed an issue which prevented `RoomMember.name` being
disambiguated if there was exactly 1 other person with the same display name.
* Member name bugfix: Disambiguate both clashing display names with user IDs in
the event of a clash.
* Room state bugfix: Fixed a bug which incorrectly overwrote power levels
locally for a room.
* Room name bugfix: Ignore users who have left the room when determining a room
name.
* Events bugfix: Fixed a bug which prevented the `sender` and `target`
properties from being set.
Changes in 0.1.1
================
+7 -2
View File
@@ -2,8 +2,8 @@ Matrix Javascript SDK
=====================
[![Build Status](http://matrix.org/jenkins/buildStatus/icon?job=JavascriptSDK)](http://matrix.org/jenkins/job/JavascriptSDK/)
This is the [Matrix](https://matrix.org) Client-Server v1/v2 alpha SDK for JavaScript. This SDK can be run
in a browser or in Node.js.
This is the [Matrix](https://matrix.org) Client-Server v1/v2 alpha SDK for
JavaScript. This SDK can be run in a browser or in Node.js.
Quickstart
==========
@@ -53,7 +53,9 @@ events for incoming data and state changes. Aside from wrapping the HTTP API, it
- Automatically retry requests to send messages due to rate limiting errors.
- Handle queueing of messages.
- Handles pagination.
- Handle assigning push actions for events.
- Handles room initial sync on accepting invites.
- Handles WebRTC calling.
Later versions of the SDK will:
- Expose a `RoomSummary` which would be suitable for a recents page.
@@ -86,6 +88,9 @@ are updated.
console.log(member.name + " stopped typing.");
}
});
// start the client to setup the connection to the server
client.startClient();
```
### Promises or Callbacks
+9900
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+101 -22
View File
@@ -86,6 +86,21 @@ rl.on('line', function(line) {
print("/invite Error: %s", err);
});
}
else if (line.indexOf("/file ") === 0) {
var filename = line.split(" ")[1].trim();
var stream = fs.createReadStream(filename);
matrixClient.uploadContent({
stream: stream,
name: filename
}).done(function(url) {
var content = {
msgtype: "m.file",
body: filename,
url: JSON.parse(url).content_uri
};
matrixClient.sendMessage(viewingRoom.roomId, content);
});
}
else {
matrixClient.sendTextMessage(viewingRoom.roomId, line).finally(function() {
printMessages();
@@ -102,7 +117,7 @@ rl.on('line', function(line) {
if (viewingRoom.getMember(myUserId).membership === "invite") {
// join the room first
matrixClient.joinRoom(viewingRoom.roomId).done(function(room) {
roomList = matrixClient.getRooms();
setRoomList();
viewingRoom = room;
printMessages();
rl.prompt();
@@ -121,14 +136,14 @@ rl.on('line', function(line) {
// show the room list after syncing.
matrixClient.on("syncComplete", function() {
roomList = matrixClient.getRooms();
setRoomList();
printRoomList();
printHelp();
rl.prompt();
});
matrixClient.on("Room", function() {
roomList = matrixClient.getRooms();
setRoomList();
if (!viewingRoom) {
printRoomList();
rl.prompt();
@@ -146,29 +161,70 @@ matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline) {
printLine(event);
});
function setRoomList() {
roomList = matrixClient.getRooms();
roomList.sort(function(a,b) {
// < 0 = a comes first (lower index) - we want high indexes = newer
var aMsg = a.timeline[a.timeline.length-1];
if (!aMsg) {
return -1;
}
var bMsg = b.timeline[b.timeline.length-1];
if (!bMsg) {
return 1;
}
if (aMsg.getTs() > bMsg.getTs()) {
return 1;
}
else if (aMsg.getTs() < bMsg.getTs()) {
return -1;
}
return 0;
});
}
function printRoomList() {
print(CLEAR_CONSOLE);
print("Room List:");
var fmts = {
"invite": clc.cyanBright,
"leave": clc.blackBright
};
for (var i = 0; i < roomList.length; i++) {
var msg = roomList[i].timeline[roomList[i].timeline.length-1];
var dateStr = "---";
var fmt;
if (msg) {
dateStr = new Date(msg.getTs()).toISOString().replace(
/T/, ' ').replace(/\..+/, '');
}
var me = roomList[i].getMember(myUserId);
if (me) {
fmt = fmts[me.membership];
}
var roomName = fixWidth(roomList[i].name, 25);
print(
"[%s] %s (%s members)",
i, roomList[i].name, roomList[i].getJoinedMembers().length
"[%s] %s (%s members) %s",
i, fmt ? fmt(roomName) : roomName,
roomList[i].getJoinedMembers().length,
dateStr
);
}
}
function printHelp() {
var hlp = clc.italic;
var hlp = clc.italic.white;
print("Global commands:", hlp);
print(" '/help' : Show this help.");
print(" '/help' : Show this help.", clc.white);
print("Room list index commands:", hlp);
print(" '/join <index>' Join a room, e.g. '/join 5'");
print(" '/join <index>' Join a room, e.g. '/join 5'", clc.white);
print("Room commands:", hlp);
print(" '/exit' Return to the room list index.");
print(" '/members' Show the room member list.");
print(" '/invite @foo:bar' Invite @foo:bar to the room.");
print(" '/more 15' Scrollback 15 events");
print(" '/resend' Resend the oldest event which failed to send.");
print(" '/roominfo' Display room info e.g. name, topic.");
print(" '/exit' Return to the room list index.", clc.white);
print(" '/members' Show the room member list.", clc.white);
print(" '/invite @foo:bar' Invite @foo:bar to the room.", clc.white);
print(" '/more 15' Scrollback 15 events", clc.white);
print(" '/resend' Resend the oldest event which failed to send.", clc.white);
print(" '/roominfo' Display room info e.g. name, topic.", clc.white);
}
function completer(line) {
@@ -193,6 +249,12 @@ function printMessages() {
}
function printMemberList(room) {
var fmts = {
"join": clc.green,
"ban": clc.red,
"invite": clc.blue,
"leave": clc.blackBright
};
var members = room.currentState.getMembers();
// sorted based on name.
members.sort(function(a, b) {
@@ -210,12 +272,15 @@ function printMemberList(room) {
if (!member.membership) {
return;
}
var fmt = fmts[member.membership] || function(a){return a;};
var membershipWithPadding = (
member.membership + new Array(10 - member.membership.length).join(" ")
);
print(
"%s :: %s (%s)", membershipWithPadding, member.name,
(member.userId === myUserId ? "Me" : member.userId)
"%s"+fmt(" :: ")+"%s"+fmt(" (")+"%s"+fmt(")"),
membershipWithPadding, member.name,
(member.userId === myUserId ? "Me" : member.userId),
fmt
);
});
}
@@ -288,6 +353,7 @@ function printLine(event) {
"[State: "+stateName+" updated to: "+JSON.stringify(event.getContent())+"]"
);
separator = "---";
fmt = clc.xterm(249).italic;
}
else {
// random message event
@@ -295,10 +361,11 @@ function printLine(event) {
"[Message: "+event.getType()+" Content: "+JSON.stringify(event.getContent())+"]"
);
separator = "---";
fmt = clc.xterm(249).italic;
}
if (fmt) {
print(
"[%s] %s %s %s", fmt(time), fmt(name), fmt(separator), fmt(body)
"[%s] %s %s %s", time, name, separator, body, fmt
);
}
else {
@@ -307,11 +374,23 @@ function printLine(event) {
}
function print(str, formatter) {
if (arguments.length == 2 && typeof arguments[1] === "function") {
console.log(arguments[1](str));
return;
if (typeof arguments[arguments.length-1] === "function") {
// last arg is the formatter so get rid of it and use it on each
// param passed in but not the template string.
var newArgs = [];
var i = 0;
for (i=0; i<arguments.length-1; i++) {
newArgs.push(arguments[i]);
}
var fmt = arguments[arguments.length-1];
for (i=0; i<newArgs.length; i++) {
newArgs[i] = fmt(newArgs[i]);
}
console.log.apply(console.log, newArgs);
}
else {
console.log.apply(console.log, arguments);
}
console.log.apply(console.log, arguments);
}
function fixWidth(str, len) {
@@ -324,4 +403,4 @@ function fixWidth(str, len) {
return str;
}
matrixClient.startClient(numMessagesToShow); // messages for each room.
matrixClient.startClient(numMessagesToShow); // messages for each room.
+9
View File
@@ -0,0 +1,9 @@
To try it out, **you must build the SDK first** and then host this folder:
```
$ npm run build
$ cd examples/voip
$ python -m SimpleHTTPServer 8003
```
Then visit ``http://localhost:8003``.
+89
View File
@@ -0,0 +1,89 @@
"use strict";
console.log("Loading browser sdk");
var BASE_URL = "https://matrix.org";
var TOKEN = "accesstokengoeshere";
var USER_ID = "@username:localhost";
var ROOM_ID = "!room:id";
var client = matrixcs.createClient({
baseUrl: BASE_URL,
accessToken: TOKEN,
userId: USER_ID
});
var call;
function disableButtons(place, answer, hangup) {
document.getElementById("hangup").disabled = hangup;
document.getElementById("answer").disabled = answer;
document.getElementById("call").disabled = place;
}
function addListeners(call) {
var lastError = "";
call.on("hangup", function() {
disableButtons(false, true, true);
document.getElementById("result").innerHTML = (
"<p>Call ended. Last error: "+lastError+"</p>"
);
});
call.on("error", function(err) {
lastError = err.message;
call.hangup();
disableButtons(false, true, true);
});
}
window.onload = function() {
document.getElementById("result").innerHTML = "<p>Please wait. Syncing...</p>";
document.getElementById("config").innerHTML = "<p>" +
"Homeserver: <code>"+BASE_URL+"</code><br/>"+
"Room: <code>"+ROOM_ID+"</code><br/>"+
"User: <code>"+USER_ID+"</code><br/>"+
"</p>";
disableButtons(true, true, true);
};
client.on("syncComplete", function () {
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
disableButtons(false, true, true);
document.getElementById("call").onclick = function() {
console.log("Placing call...");
call = matrixcs.createNewMatrixCall(
client, ROOM_ID
);
console.log("Call => %s", call);
addListeners(call);
call.placeVideoCall(
document.getElementById("remote"),
document.getElementById("local")
);
document.getElementById("result").innerHTML = "<p>Placed call.</p>";
disableButtons(true, true, false);
};
document.getElementById("hangup").onclick = function() {
console.log("Hanging up call...");
console.log("Call => %s", call);
call.hangup();
document.getElementById("result").innerHTML = "<p>Hungup call.</p>";
};
document.getElementById("answer").onclick = function() {
console.log("Answering call...");
console.log("Call => %s", call);
call.answer();
disableButtons(true, true, false);
document.getElementById("result").innerHTML = "<p>Answered call.</p>";
};
client.on("Call.incoming", function(c) {
console.log("Call ringing");
disableButtons(true, false, false);
document.getElementById("result").innerHTML = "<p>Incoming call...</p>";
call = c;
addListeners(call);
});
});
client.startClient();
+26
View File
@@ -0,0 +1,26 @@
<html>
<head>
<title>VoIP Test</title>
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
<body>
You can place and receive calls with this example. Make sure to edit the
constants in <code>browserTest.js</code> first.
<div id="config"></div>
<div id="result"></div>
<button id="call">Place Call</button>
<button id="answer">Answer Call</button>
<button id="hangup">Hangup Call</button>
<div id="videoBackground">
<div id="videoContainer">
<video id="remote"></video>
</div>
</div>
<div id="videoBackground">
<div id="videoContainer">
<video id="local"></video>
</div>
</div>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
../../../dist/browser-matrix-dev.js
+1120 -82
View File
File diff suppressed because it is too large Load Diff
+131 -6
View File
@@ -10,7 +10,6 @@ var utils = require("./utils");
TODO:
- CS: complete register function (doing stages)
- Identity server: linkEmail, authEmail, bindEmail, lookup3pid
- uploadContent (?)
*/
/**
@@ -22,7 +21,12 @@ module.exports.PREFIX_V1 = "/_matrix/client/api/v1";
* A constant representing the URI path for version 2 alpha of the Client-Server
* HTTP API.
*/
module.exports.PREFIX_V2_ALPHA_PREFIX = "/_matrix/client/v2_alpha";
module.exports.PREFIX_V2_ALPHA = "/_matrix/client/v2_alpha";
/**
* URI path for the identity API
*/
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
/**
* Construct a MatrixHttpApi.
@@ -39,6 +43,8 @@ module.exports.PREFIX_V2_ALPHA_PREFIX = "/_matrix/client/v2_alpha";
* codes and headers in addition to data. Default: false.
* @param {string} opts.accessToken The access_token to send with requests. Can be
* null to not send an access token.
* @param {Object} opts.extraParams Optional. Extra query parameters to send on
* requests.
*/
module.exports.MatrixHttpApi = function MatrixHttpApi(opts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
@@ -92,7 +98,7 @@ module.exports.MatrixHttpApi.prototype = {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
}
return this.credentials.baseUrl + prefix + serverAndMediaId +
return this.opts.baseUrl + prefix + serverAndMediaId +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params))) + fragment;
},
@@ -118,7 +124,7 @@ module.exports.MatrixHttpApi.prototype = {
var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
$ident: identiconString
});
return this.credentials.baseUrl + path +
return this.opts.baseUrl + path +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params)));
},
@@ -130,15 +136,123 @@ module.exports.MatrixHttpApi.prototype = {
*/
getContentUri: function() {
var params = {
access_token: this.credentials.accessToken
access_token: this.opts.accessToken
};
return {
base: this.credentials.baseUrl,
base: this.opts.baseUrl,
path: "/_matrix/media/v1/upload",
params: params
};
},
/**
* Upload content to the Home Server
* @param {File} file A File object (in a browser) or in Node,
an object with properties:
name: The file's name
stream: A read stream
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
*/
uploadContent: function(file, callback) {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
}
var defer = q.defer();
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
// browser-request doesn't support File objects because it deep-copies
// the options using JSON.parse(JSON.stringify(options)). Instead of
// loading the whole file into memory as a string and letting
// browser-request base64 encode and then decode it again, we just
// use XMLHttpRequest directly.
// (browser-request doesn't support progress either, which is also kind
// of important here)
if (global.XMLHttpRequest) {
var xhr = new global.XMLHttpRequest();
var cb = requestCallback(defer, callback, this.opts.onlyData);
var timeout_fn = function() {
xhr.abort();
cb(new Error('Timeout'));
};
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
clearTimeout(xhr.timeout_timer);
var resp = JSON.parse(xhr.responseText);
if (resp.content_uri === undefined) {
cb(new Error('Bad response'));
return;
}
cb(undefined, xhr, resp.content_uri);
break;
}
};
xhr.upload.addEventListener("progress", function(ev) {
clearTimeout(xhr.timeout_timer);
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
defer.notify(ev);
});
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
url += "&filename=" + encodeURIComponent(file.name);
xhr.open("POST", url);
xhr.send(file);
} else {
var queryParams = {
filename: file.name,
access_token: this.opts.accessToken
};
file.stream.pipe(
this.opts.request({
uri: url,
qs: queryParams,
method: "POST"
}, requestCallback(defer, callback, this.opts.onlyData))
);
}
return defer.promise;
},
idServerRequest: function(callback, method, path, params, prefix) {
var fullUri = this.opts.idBaseUrl + prefix + path;
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
}
var opts = {
uri: fullUri,
method: method,
withCredentials: false,
json: false,
_matrix_opts: this.opts
};
if (method == 'GET') {
opts.qs = params;
} else {
opts.form = params;
}
var defer = q.defer();
this.opts.request(
opts,
requestCallback(defer, callback, this.opts.onlyData)
);
return defer.promise;
},
/**
* Perform an authorised request to the homeserver.
* @param {Function} callback Optional. The callback to invoke on
@@ -251,6 +365,15 @@ module.exports.MatrixHttpApi.prototype = {
"Expected callback to be a function but got " + typeof callback
);
}
if (!queryParams) {
queryParams = {};
}
if (this.opts.extraParams) {
for (var key in this.opts.extraParams) {
if (!this.opts.extraParams.hasOwnProperty(key)) { continue; }
queryParams[key] = this.opts.extraParams[key];
}
}
var defer = q.defer();
this.opts.request(
{
@@ -275,6 +398,7 @@ var requestCallback = function(defer, userDefinedCallback, onlyData) {
return function(err, response, body) {
if (!err && response.statusCode >= 400) {
err = new module.exports.MatrixError(body);
err.httpStatus = response.statusCode;
}
if (err) {
@@ -301,6 +425,7 @@ var requestCallback = function(defer, userDefinedCallback, onlyData) {
* @prop {string} name The Matrix 'errcode' value, e.g. "M_FORBIDDEN".
* @prop {string} message The Matrix 'error' value, e.g. "Missing token."
* @prop {Object} data The raw Matrix error JSON used to construct this object.
* @prop {integer} httpStatus The numeric HTTP status code given
*/
module.exports.MatrixError = function MatrixError(errorJson) {
this.name = errorJson.errcode || "Unknown error code";
+29 -8
View File
@@ -6,6 +6,9 @@ module.exports.MatrixEvent = require("./models/event").MatrixEvent;
module.exports.EventStatus = require("./models/event").EventStatus;
/** The {@link module:store/memory.MatrixInMemoryStore|MatrixInMemoryStore} class. */
module.exports.MatrixInMemoryStore = require("./store/memory").MatrixInMemoryStore;
/** The {@link module:store/webstorage~WebStorageStore|WebStorageStore} class.
* <strong>Work in progress; unstable.</strong> */
module.exports.WebStorageStore = require("./store/webstorage");
/** The {@link module:http-api.MatrixHttpApi|MatrixHttpApi} class. */
module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
/** The {@link module:http-api.MatrixError|MatrixError} class. */
@@ -22,6 +25,21 @@ module.exports.RoomState = require("./models/room-state");
module.exports.User = require("./models/user");
/** The {@link module:scheduler~MatrixScheduler|MatrixScheduler} class. */
module.exports.MatrixScheduler = require("./scheduler");
/** The {@link module:store/session/webstorage~WebStorageSessionStore|
* WebStorageSessionStore} class. <strong>Work in progress; unstable.</strong> */
module.exports.WebStorageSessionStore = require("./store/session/webstorage");
/** True if crypto libraries are being used on this client. */
module.exports.CRYPTO_ENABLED = require("./client").CRYPTO_ENABLED;
/**
* Create a new Matrix Call.
* @function
* @param {module:client.MatrixClient} client The MatrixClient instance to use.
* @param {string} roomId The room the call is in.
* @return {module:webrtc/call~MatrixCall} The Matrix call or null if the browser
* does not support WebRTC.
*/
module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCall;
// expose the underlying request object so different environments can use
// different request libs (e.g. request or browser-request)
@@ -42,12 +60,15 @@ module.exports.request = function(r) {
* @param {(Object|string)} opts The configuration options for this client. If
* this is a string, it is assumed to be the base URL. These configuration
* options will be passed directly to {@link module:client~MatrixClient}.
* @param {string} opts.baseUrl The base URL to the client-server HTTP API.
* @param {string} opts.accessToken Optional. The access_token for this user.
* @param {string} opts.userId Optional. The user ID for this user.
* @param {Object} opts.store Set to {@link module:store/memory.MatrixInMemoryStore}.
* @param {Object} opts.scheduler Set to {@link module:scheduler~MatrixScheduler}.
* @param {Object} opts.store If not set, defaults to
* {@link module:store/memory.MatrixInMemoryStore}.
* @param {Object} opts.scheduler If not set, defaults to
* {@link module:scheduler~MatrixScheduler}.
* @param {requestFunction} opts.request If not set, defaults to the function
* supplied to {@link request} which defaults to the request module from NPM.
* @return {MatrixClient} A new matrix client.
* @see {@link module:client~MatrixClient} for the full list of options for
* <code>opts</code>.
*/
module.exports.createClient = function(opts) {
if (typeof opts === "string") {
@@ -55,9 +76,9 @@ module.exports.createClient = function(opts) {
"baseUrl": opts
};
}
opts.request = request;
opts.store = new module.exports.MatrixInMemoryStore();
opts.scheduler = new module.exports.MatrixScheduler();
opts.request = opts.request || request;
opts.store = opts.store || new module.exports.MatrixInMemoryStore();
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
return new module.exports.MatrixClient(opts);
};
+31 -2
View File
@@ -1,4 +1,5 @@
"use strict";
/**
* This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for
* the public classes.
@@ -15,13 +16,16 @@ module.exports.EventStatus = {
/** The event was not sent and will no longer be retried. */
NOT_SENT: "not_sent",
/** The event is in the process of being sent. */
SENDING: "sending"
SENDING: "sending",
/** The event is in a queue waiting to be sent. */
QUEUED: "queued"
};
/**
* Construct a Matrix Event object
* @constructor
* @param {Object} event The raw event to be wrapped in this DAO
* @param {boolean} encrypted Was the event encrypted
* @prop {Object} event The raw event. <b>Do not access this property</b>
* directly unless you absolutely have to. Prefer the getter methods defined on
* this class. Using the getter methods shields your app from
@@ -35,12 +39,13 @@ module.exports.EventStatus = {
* that getDirectionalContent() will return event.content and not event.prev_content.
* Default: true. <strong>This property is experimental and may change.</strong>
*/
module.exports.MatrixEvent = function MatrixEvent(event) {
module.exports.MatrixEvent = function MatrixEvent(event, encrypted) {
this.event = event || {};
this.sender = null;
this.target = null;
this.status = null;
this.forwardLooking = true;
this.encrypted = Boolean(encrypted);
};
module.exports.MatrixEvent.prototype = {
@@ -69,6 +74,14 @@ module.exports.MatrixEvent.prototype = {
return this.event.type;
},
/**
* Get the type of the event that will be sent to the homeserver.
* @return {string} The event type.
*/
getWireType: function() {
return this.encryptedType || this.event.type;
},
/**
* Get the room_id for this event. This will return <code>undefined</code>
* for <code>m.presence</code> events.
@@ -95,6 +108,14 @@ module.exports.MatrixEvent.prototype = {
return this.event.content || {};
},
/**
* Get the event content JSON that will be sent to the homeserver.
* @return {Object} The event content JSON, or an empty object.
*/
getWireContent: function() {
return this.encryptedContent || this.event.content || {};
},
/**
* Get the previous event content JSON. This will only return something for
* state events which exist in the timeline.
@@ -140,5 +161,13 @@ module.exports.MatrixEvent.prototype = {
*/
isState: function() {
return this.event.state_key !== undefined;
},
/**
* Check if the event is encrypted.
* @return {boolean} True if this event is encrypted.
*/
isEncrypted: function() {
return this.encrypted;
}
};
+18 -1
View File
@@ -128,9 +128,26 @@ RoomMember.prototype.setTypingEvent = function(event) {
function calculateDisplayName(member, event, roomState) {
var displayName = event.getDirectionalContent().displayname;
var selfUserId = member.userId;
/*
// FIXME: this would be great but still needs to use the
// full userId to disambiguate if needed...
if (!displayName) {
var matches = selfUserId.match(/^@(.*?):/);
if (matches) {
return matches[1];
}
else {
return selfUserId;
}
}
*/
if (!displayName) {
return selfUserId;
}
if (!roomState) {
return displayName;
}
@@ -142,7 +159,7 @@ function calculateDisplayName(member, event, roomState) {
e.getSender() !== selfUserId;
}
);
if (stateEvents.length > 1) {
if (stateEvents.length > 0) {
// need to disambiguate
return displayName + " (" + selfUserId + ")";
}
+12
View File
@@ -91,10 +91,13 @@ RoomState.prototype.getStateEvents = function(eventType, stateKey) {
* if there are <code>m.room.member</code> events.
* @param {MatrixEvent[]} stateEvents a list of state events for this room.
* @fires module:client~MatrixClient#event:"RoomState.members"
* @fires module:client~MatrixClient#event:"RoomState.newMember"
* @fires module:client~MatrixClient#event:"RoomState.events"
*/
RoomState.prototype.setStateEvents = function(stateEvents) {
var self = this;
// update the core event dict
utils.forEach(stateEvents, function(event) {
if (event.getRoomId() !== self.roomId) { return; }
if (!event.isState()) { return; }
@@ -104,6 +107,15 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
}
self.events[event.getType()][event.getStateKey()] = event;
self.emit("RoomState.events", event, self);
});
// update higher level data structures. This needs to be done AFTER the
// core event dict as these structures may depend on other state events in
// the given array (e.g. disambiguating display names in one go to do both
// clashing names rather than progressively which only catches 1 of them).
utils.forEach(stateEvents, function(event) {
if (event.getRoomId() !== self.roomId) { return; }
if (!event.isState()) { return; }
if (event.getType() === "m.room.member") {
var userId = event.getStateKey();
+85 -23
View File
@@ -12,6 +12,9 @@ var utils = require("../utils");
* Construct a new Room.
* @constructor
* @param {string} roomId Required. The ID of this room.
* @param {*} 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
* implementation.
* @prop {string} roomId The ID of this room.
* @prop {string} name The human-readable display name for this room.
* @prop {Array<MatrixEvent>} timeline The ordered list of message events for
@@ -21,14 +24,18 @@ var utils = require("../utils");
* @prop {RoomState} currentState The state of the room at the time of the
* newest event in the timeline.
* @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) {
function Room(roomId, storageToken) {
this.roomId = roomId;
this.name = roomId;
this.timeline = [];
this.oldState = new RoomState(roomId);
this.currentState = new RoomState(roomId);
this.summary = null;
this.storageToken = storageToken;
this._redactions = [];
}
utils.inherits(Room, EventEmitter);
@@ -50,8 +57,17 @@ utils.inherits(Room, EventEmitter);
* @return {RoomMember[]} A list of currently joined members.
*/
Room.prototype.getJoinedMembers = function() {
return this.getMembersWithMemership("join");
};
/**
* Get a list of members with given membership state.
* @param {string} membership The membership state.
* @return {RoomMember[]} A list of members with the given membership state.
*/
Room.prototype.getMembersWithMemership = function(membership) {
return utils.filter(this.currentState.getMembers(), function(m) {
return m.membership === "join";
return m.membership === membership;
});
};
@@ -78,28 +94,43 @@ utils.inherits(Room, EventEmitter);
*/
Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline) {
var stateContext = toStartOfTimeline ? this.oldState : this.currentState;
function checkForRedaction(redactEvent) {
return function(e) {
return e.getId() === redactEvent.event.redacts;
};
}
for (var i = 0; i < events.length; i++) {
// set sender and target properties
events[i].sender = stateContext.getSentinelMember(
events[i].getSender()
);
if (events[i].getType() === "m.room.member") {
events[i].target = stateContext.getSentinelMember(
events[i].getStateKey()
);
if (toStartOfTimeline && this._redactions.indexOf(events[i].getId()) >= 0) {
continue; // do not add the redacted event.
}
setEventMetadata(events[i], stateContext, toStartOfTimeline);
// modify state
if (events[i].isState()) {
// room state has no concept of 'old' or 'current', but we want the
// room state to regress back to previous values if toStartOfTimeline
// is set, which means inspecting prev_content if it exists. This
// is done by toggling the forwardLooking flag.
if (toStartOfTimeline) {
events[i].forwardLooking = false;
stateContext.setStateEvents([events[i]]);
// it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try
// it again if the prop wasn't previously set.
if (!events[i].sender) {
setEventMetadata(events[i], stateContext, toStartOfTimeline);
}
stateContext.setStateEvents([events[i]], toStartOfTimeline);
}
if (events[i].getType() === "m.room.redaction") {
// try to remove the element
var removed = utils.removeElement(
this.timeline, checkForRedaction(events[i])
);
if (!removed && toStartOfTimeline) {
// redactions will trickle in BEFORE the event redacted so make
// a note of the redacted event; we'll check it later.
this._redactions.push(events[i].event.redacts);
}
// NB: We continue to add the redaction event to the timeline so clients
// can say "so and so redacted an event" if they wish to.
}
// TODO: pass through filter to see if this should be added to the timeline.
if (toStartOfTimeline) {
this.timeline.unshift(events[i]);
@@ -107,7 +138,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline) {
else {
this.timeline.push(events[i]);
}
this.emit("Room.timeline", events[i], this, toStartOfTimeline);
this.emit("Room.timeline", events[i], this, Boolean(toStartOfTimeline));
}
};
@@ -140,7 +171,15 @@ Room.prototype.addEvents = function(events, duplicateStrategy) {
for (var j = 0; j < this.timeline.length; j++) {
if (this.timeline[j].getId() === events[i].getId()) {
if (duplicateStrategy === "replace") {
this.timeline[j] = events[i];
// still need to set the right metadata on this event
setEventMetadata(
events[i],
this.currentState,
false
);
if (!this.timeline[j].encryptedType) {
this.timeline[j] = events[i];
}
// skip the insert so we don't add this event twice.
// Don't break in case we replace multiple events.
shouldIgnore = true;
@@ -181,6 +220,27 @@ Room.prototype.recalculate = function(userId) {
}
};
function setEventMetadata(event, stateContext, toStartOfTimeline) {
// set sender and target properties
event.sender = stateContext.getSentinelMember(
event.getSender()
);
if (event.getType() === "m.room.member") {
event.target = stateContext.getSentinelMember(
event.getStateKey()
);
}
if (event.isState()) {
// room state has no concept of 'old' or 'current', but we want the
// room state to regress back to previous values if toStartOfTimeline
// is set, which means inspecting prev_content if it exists. This
// is done by toggling the forwardLooking flag.
if (toStartOfTimeline) {
event.forwardLooking = false;
}
}
}
/**
* This is an internal method. Calculates the name of the room from the current
* room state.
@@ -200,19 +260,21 @@ function calculateRoomName(room, userId) {
var mRoomName = room.currentState.getStateEvents('m.room.name', '');
if (mRoomName) {
return mRoomName.getContent().name + (alias ? " (" + alias + ")" : "");
return mRoomName.getContent().name + (false && alias ? " (" + alias + ")" : "");
}
else if (alias) {
return alias;
}
else {
// get members that are NOT ourselves.
// get members that are NOT ourselves and are actually in the room.
var members = utils.filter(room.currentState.getMembers(), function(m) {
return m.userId !== userId;
return (m.userId !== userId && m.membership !== "leave");
});
// TODO: Localisation
if (members.length === 0) {
var memberList = room.currentState.getMembers();
var memberList = utils.filter(room.currentState.getMembers(), function(m) {
return (m.membership !== "leave");
});
if (memberList.length === 1) {
// we exist, but no one else... self-chat or invite.
if (memberList[0].membership === "invite") {
+2 -1
View File
@@ -45,10 +45,11 @@ User.prototype.setPresenceEvent = function(event) {
if (event.getType() !== "m.presence") {
return;
}
var firstFire = this.events.presence === null;
this.events.presence = event;
var eventsToFire = [];
if (event.getContent().presence !== this.presence) {
if (event.getContent().presence !== this.presence || firstFire) {
eventsToFire.push("User.presence");
}
if (event.getContent().avatar_url !== this.avatarUrl) {
+258
View File
@@ -0,0 +1,258 @@
/**
* @module pushprocessor
*/
/**
* Construct a Push Processor.
* @constructor
* @param {Object} client The Matrix client object to use
*/
function PushProcessor(client) {
var escapeRegExp = function(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
var matchingRuleFromKindSet = function(ev, kindset, device) {
var rulekinds_in_order = ['override', 'content', 'room', 'sender', 'underride'];
for (var ruleKindIndex = 0;
ruleKindIndex < rulekinds_in_order.length;
++ruleKindIndex) {
var kind = rulekinds_in_order[ruleKindIndex];
var ruleset = kindset[kind];
for (var ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
var rule = ruleset[ruleIndex];
if (!rule.enabled) { continue; }
var rawrule = templateRuleToRaw(kind, rule, device);
if (!rawrule) { continue; }
if (ruleMatchesEvent(rawrule, ev)) {
rule.kind = kind;
return rule;
}
}
}
return null;
};
var templateRuleToRaw = function(kind, tprule, device) {
var rawrule = {
'rule_id': tprule.rule_id,
'actions': tprule.actions,
'conditions': []
};
switch (kind) {
case 'underride':
case 'override':
rawrule.conditions = tprule.conditions;
break;
case 'room':
if (!tprule.rule_id) { return null; }
rawrule.conditions.push({
'kind': 'event_match',
'key': 'room_id',
'pattern': tprule.rule_id
});
break;
case 'sender':
if (!tprule.rule_id) { return null; }
rawrule.conditions.push({
'kind': 'event_match',
'key': 'user_id',
'pattern': tprule.rule_id
});
break;
case 'content':
if (!tprule.pattern) { return null; }
rawrule.conditions.push({
'kind': 'event_match',
'key': 'content.body',
'pattern': tprule.pattern
});
break;
}
if (device) {
rawrule.conditions.push({
'kind': 'device',
'profile_tag': device
});
}
return rawrule;
};
var ruleMatchesEvent = function(rule, ev) {
var ret = true;
for (var i = 0; i < rule.conditions.length; ++i) {
var cond = rule.conditions[i];
ret &= eventFulfillsCondition(cond, ev);
}
//console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match"));
return ret;
};
var eventFulfillsCondition = function(cond, ev) {
var condition_functions = {
"event_match": eventFulfillsEventMatchCondition,
"device": eventFulfillsDeviceCondition,
"contains_display_name": eventFulfillsDisplayNameCondition,
"room_member_count": eventFulfillsRoomMemberCountCondition
};
if (condition_functions[cond.kind]) {
return condition_functions[cond.kind](cond, ev);
}
return true;
};
var eventFulfillsRoomMemberCountCondition = function(cond, ev) {
if (!cond.is) { return false; }
var room = client.getRoom(ev.room_id);
if (!room || !room.currentState || !room.currentState.members) { return false; }
var memberCount = Object.keys(room.currentState.members).length;
var m = cond.is.match(/^([=<>]*)([0-9]*)$/);
if (!m) { return false; }
var ineq = m[1];
var rhs = parseInt(m[2]);
if (isNaN(rhs)) { return false; }
switch (ineq) {
case '':
case '==':
return memberCount == rhs;
case '<':
return memberCount < rhs;
case '>':
return memberCount > rhs;
case '<=':
return memberCount <= rhs;
case '>=':
return memberCount >= rhs;
default:
return false;
}
};
var eventFulfillsDisplayNameCondition = function(cond, ev) {
if (!ev.content || ! ev.content.body || typeof ev.content.body != 'string') {
return false;
}
var room = client.getRoom(ev.room_id);
if (!room || !room.currentState || !room.currentState.members) { return false; }
var displayName = room.currentState.getMember(client.credentials.userId).name;
var pat = new RegExp("\\b" + escapeRegExp(displayName) + "\\b", 'i');
return ev.content.body.search(pat) > -1;
};
var eventFulfillsDeviceCondition = function(cond, ev) {
return false; // XXX: Allow a profile tag to be set for the web client instance
};
var eventFulfillsEventMatchCondition = function(cond, ev) {
var val = valueForDottedKey(cond.key, ev);
if (!val || typeof val != 'string') { return false; }
var pat;
if (cond.key == 'content.body') {
pat = '\\b' + globToRegexp(cond.pattern) + '\\b';
} else {
pat = '^' + globToRegexp(cond.pattern) + '$';
}
var regex = new RegExp(pat, 'i');
return !!val.match(regex);
};
var globToRegexp = function(glob) {
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
// Because micromatch is about 130KB with dependencies,
// and minimatch is not much better.
var pat = escapeRegExp(glob);
pat = pat.replace(/\\\*/, '.*');
pat = pat.replace(/\?/, '.');
pat = pat.replace(/\\\[(!|)(.*)\\]/, function(match, p1, p2, offset, string) {
var first = p1 && '^' || '';
var second = p2.replace(/\\\-/, '-');
return '[' + first + second + ']';
});
return pat;
};
var valueForDottedKey = function(key, ev) {
var parts = key.split('.');
var val = ev;
while (parts.length > 0) {
var thispart = parts.shift();
if (!val[thispart]) { return null; }
val = val[thispart];
}
return val;
};
var matchingRuleForEventWithRulesets = function(ev, rulesets) {
if (!rulesets) { return null; }
if (ev.user_id == client.credentials.userId) { return null; }
var allDevNames = Object.keys(rulesets.device);
for (var i = 0; i < allDevNames.length; ++i) {
var devname = allDevNames[i];
var devrules = rulesets.device[devname];
var matchingRule = matchingRuleFromKindSet(devrules, devname);
if (matchingRule) { return matchingRule; }
}
return matchingRuleFromKindSet(ev, rulesets.global);
};
var actionListToActionsObject = function(actionlist) {
var actionobj = { 'notify': false, 'tweaks': {} };
for (var i = 0; i < actionlist.length; ++i) {
var action = actionlist[i];
if (action === 'notify') {
actionobj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) { action.value = true; }
actionobj.tweaks[action.set_tweak] = action.value;
}
}
return actionobj;
};
var pushActionsForEventAndRulesets = function(ev, rulesets) {
var rule = matchingRuleForEventWithRulesets(ev, rulesets);
if (!rule) { return {}; }
var actionObj = actionListToActionsObject(rule.actions);
// Some actions are implicit in some situations: we add those here
if (actionObj.tweaks.highlight === undefined) {
// if it isn't specified, highlight if it's a content
// rule but otherwise not
actionObj.tweaks.highlight = (rule.kind == 'content');
}
return actionObj;
};
this.actionsForEvent = function(ev) {
return pushActionsForEventAndRulesets(ev, client.pushRules);
};
}
/**
* @typedef {Object} PushAction
* @type {Object}
* @property {boolean} notify Whether this event should notify the user or not.
* @property {Object} tweaks How this event should be notified.
* @property {boolean} tweaks.highlight Whether this event should be highlighted
* on the UI.
* @property {boolean} tweaks.sound Whether this notification should produce a
* noise.
*/
/** The PushProcessor class. */
module.exports = PushProcessor;
+47 -5
View File
@@ -35,6 +35,47 @@ function MatrixScheduler(retryAlgorithm, queueAlgorithm) {
this._procFn = null;
}
/**
* Retrieve a queue based on an event. The event provided does not need to be in
* the queue.
* @param {MatrixEvent} event An event to get the queue for.
* @return {?Array<MatrixEvent>} A shallow copy of events in the queue or null.
* Modifying this array will not modify the list itself. Modifying events in
* this array <i>will</i> modify the underlying event in the queue.
* @see MatrixScheduler.removeEventFromQueue To remove an event from the queue.
*/
MatrixScheduler.prototype.getQueueForEvent = function(event) {
var name = this.queueAlgorithm(event);
if (!name || !this._queues[name]) {
return null;
}
return utils.map(this._queues[name], function(obj) {
return obj.event;
});
};
/**
* Remove this event from the queue. The event is equal to another event if they
* have the same ID returned from event.getId().
* @param {MatrixEvent} event The event to remove.
* @return {boolean} True if this event was removed.
*/
MatrixScheduler.prototype.removeEventFromQueue = function(event) {
var name = this.queueAlgorithm(event);
if (!name || !this._queues[name]) {
return false;
}
var removed = false;
utils.removeElement(this._queues[name], function(element) {
if (element.event.getId() === event.getId()) {
removed = true;
return true;
}
});
return removed;
};
/**
* Set the process function. Required for events in the queue to be processed.
* If set after events have been added to the queue, this will immediately start
@@ -223,11 +264,12 @@ function debuglog() {
*/
/**
* The queuing algorithm to apply to events. All queues created are serviced in
* a FIFO manner. To send the event ASAP, return <code>null</code> which will
* not put this event in a queue. Events that fail to send that form part of
* a queue will be removed from the queue and the next event in the queue will
* be sent.
* The queuing algorithm to apply to events. This function must be idempotent as
* it may be called multiple times with the same event. All queues created are
* serviced in a FIFO manner. To send the event ASAP, return <code>null</code>
* which will not put this event in a queue. Events that fail to send that form
* part of a queue will be removed from the queue and the next event in the
* queue will be sent.
* @callback queueAlgorithm
* @param {MatrixEvent} event The event to be sent.
* @return {string} The name of the queue to put the event into. If a queue with
+39
View File
@@ -16,10 +16,27 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore() {
this.users = {
// userId: User
};
this.syncToken = null;
};
module.exports.MatrixInMemoryStore.prototype = {
/**
* Retrieve the token to stream from.
* @return {string} The token or null.
*/
getSyncToken: function() {
return this.syncToken;
},
/**
* Set the token to stream from.
* @param {string} token The token to stream from.
*/
setSyncToken: function(token) {
this.syncToken = token;
},
/**
* Store the given room.
* @param {Room} room The room to be stored. All properties must be stored.
@@ -70,6 +87,28 @@ module.exports.MatrixInMemoryStore.prototype = {
*/
getUser: function(userId) {
return this.users[userId] || null;
},
/**
* Retrieve scrollback for this room.
* @param {Room} room The matrix room
* @param {integer} limit The max number of old events to retrieve.
* @return {Array<Object>} An array of objects which will be at most 'limit'
* length and at least 0. The objects are the raw event JSON.
*/
scrollback: function(room, limit) {
return [];
},
/**
* Store events for a room. The events have already been added to the timeline
* @param {Room} room The room to store events for.
* @param {Array<MatrixEvent>} events The events to store.
* @param {string} token The token associated with these events.
* @param {boolean} toStart True if these are paginated results.
*/
storeEvents: function(room, events, token, toStart) {
// no-op because they've already been added to the room instance.
}
// TODO
+147
View File
@@ -0,0 +1,147 @@
"use strict";
/**
* @module store/session/webstorage
*/
var utils = require("../../utils");
var DEBUG = false; // set true to enable console logging.
var E2E_PREFIX = "session.e2e.";
/**
* Construct a web storage session store, capable of storing account keys,
* session keys and access tokens.
* @constructor
* @param {WebStorage} webStore A web storage implementation, e.g.
* 'window.localStorage' or 'window.sessionStorage' or a custom implementation.
* @throws if the supplied 'store' does not meet the Storage interface of the
* WebStorage API.
*/
function WebStorageSessionStore(webStore) {
this.store = webStore;
if (!utils.isFunction(webStore.getItem) ||
!utils.isFunction(webStore.setItem) ||
!utils.isFunction(webStore.removeItem)) {
throw new Error(
"Supplied webStore does not meet the WebStorage API interface"
);
}
}
WebStorageSessionStore.prototype = {
/**
* Store the end to end account for the logged-in user.
* @param {string} account Base64 encoded account.
*/
storeEndToEndAccount: function(account) {
this.store.setItem(KEY_END_TO_END_ACCOUNT, account);
},
/**
* Load the end to end account for the logged-in user.
* @return {?string} Base64 encoded account.
*/
getEndToEndAccount: function() {
return this.store.getItem(KEY_END_TO_END_ACCOUNT);
},
/**
* Stores the known devices for a user.
* @param {string} userId The user's ID.
* @param {object} devices A map from device ID to keys for the device.
*/
storeEndToEndDevicesForUser: function(userId, devices) {
setJsonItem(this.store, keyEndToEndDevicesForUser(userId), devices);
},
/**
* Retrieves the known devices for a user.
* @param {string} userId The user's ID.
* @return {object} A map from device ID to keys for the device.
*/
getEndToEndDevicesForUser: function(userId) {
return getJsonItem(this.store, keyEndToEndDevicesForUser(userId));
},
/**
* Store a session between the logged-in user and another device
* @param {string} deviceKey The public key of the other device.
* @param {string} sessionId The ID for this end-to-end session.
* @param {string} session Base64 encoded end-to-end session.
*/
storeEndToEndSession: function(deviceKey, sessionId, session) {
var sessions = this.getEndToEndSessions(deviceKey) || {};
sessions[sessionId] = session;
setJsonItem(
this.store, keyEndToEndSessions(deviceKey), sessions
);
},
/**
* Retrieve the end-to-end sessions between the logged-in user and another
* device.
* @param {string} deviceKey The public key of the other device.
* @return {object} A map from sessionId to Base64 end-to-end session.
*/
getEndToEndSessions: function(deviceKey) {
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
},
/**
* Store the end-to-end state for a room.
* @param {string} roomId The room's ID.
* @param {object} roomInfo The end-to-end info for the room.
*/
storeEndToEndRoom: function(roomId, roomInfo) {
setJsonItem(this.store, keyEndToEndRoom(roomId), roomInfo);
},
/**
* Get the end-to-end state for a room
* @param {string} roomId The room's ID.
* @return {object} The end-to-end info for the room.
*/
getEndToEndRoom: function(roomId) {
return getJsonItem(this.store, keyEndToEndRoom(roomId));
}
};
var KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
function keyEndToEndDevicesForUser(userId) {
return E2E_PREFIX + "devices/" + userId;
}
function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey;
}
function keyEndToEndRoom(roomId) {
return E2E_PREFIX + "rooms/" + roomId;
}
function getJsonItem(store, key) {
try {
return JSON.parse(store.getItem(key));
}
catch (e) {
debuglog("Failed to get key %s: %s", key, e);
debuglog(e.stack);
}
return null;
}
function setJsonItem(store, key, val) {
store.setItem(key, JSON.stringify(val));
}
function debuglog() {
if (DEBUG) {
console.log.apply(console, arguments);
}
}
/** */
module.exports = WebStorageSessionStore;
+38 -2
View File
@@ -5,15 +5,31 @@
*/
/**
* Construct a stub store. This does no-ops on all store methods.
* Construct a stub store. This does no-ops on most store methods.
* @constructor
*/
function StubStore() {
this.fromToken = null;
}
StubStore.prototype = {
/**
* Get the sync token.
* @return {string}
*/
getSyncToken: function() {
return this.fromToken;
},
/**
* Set the sync token.
* @param {string} token
*/
setSyncToken: function(token) {
this.fromToken = token;
},
/**
* No-op.
* @param {Room} room
@@ -60,6 +76,26 @@ StubStore.prototype = {
*/
getUser: function(userId) {
return null;
},
/**
* No-op.
* @param {Room} room
* @param {integer} limit
* @return {Array}
*/
scrollback: function(room, limit) {
return [];
},
/**
* Store events for a room.
* @param {Room} room The room to store events for.
* @param {Array<MatrixEvent>} events The events to store.
* @param {string} token The token associated with these events.
* @param {boolean} toStart True if these are paginated results.
*/
storeEvents: function(room, events, token, toStart) {
}
// TODO
+651
View File
@@ -0,0 +1,651 @@
"use strict";
/**
* This is an internal module. Implementation details:
* <pre>
* Room data is stored as follows:
* room_$ROOMID_timeline_$INDEX : [ Event, Event, Event ]
* room_$ROOMID_state : {
* pagination_token: <oldState.paginationToken>,
* events: {
* <event_type>: { <state_key> : {JSON} }
* }
* }
* User data is stored as follows:
* user_$USERID : User
* Sync token:
* sync_token : $TOKEN
*
* Room Retrieval
* --------------
* Retrieving a room requires the $ROOMID which then pulls out the current state
* from room_$ROOMID_state. A defined starting batch of timeline events are then
* extracted from the highest numbered $INDEX for room_$ROOMID_timeline_$INDEX
* (more indices as required). The $INDEX may be negative. These are
* added to the timeline in the same way as /initialSync (old state will diverge).
* If there exists a room_$ROOMID_timeline_live key, then a timeline sync should
* be performed before retrieving.
*
* Retrieval of earlier messages
* -----------------------------
* The earliest event the Room instance knows about is E. Retrieving earlier
* messages requires a Room which has a storageToken defined.
* This token maps to the index I where the Room is at. Events are then retrieved from
* room_$ROOMID_timeline_{I} and elements before E are extracted. If the limit
* demands more events, I-1 is retrieved, up until I=min $INDEX where it gives
* less than the limit. Index may go negative if you have paginated in the past.
*
* Full Insertion
* --------------
* Storing a room requires the timeline and state keys for $ROOMID to
* be blown away and completely replaced, which is computationally expensive.
* Room.timeline is batched according to the given batch size B. These batches
* are then inserted into storage as room_$ROOMID_timeline_$INDEX. Finally,
* the current room state is persisted to room_$ROOMID_state.
*
* Incremental Insertion
* ---------------------
* As events arrive, the store can quickly persist these new events. This
* involves pushing the events to room_$ROOMID_timeline_live. If the
* current room state has been modified by the new event, then
* room_$ROOMID_state should be updated in addition to the timeline.
*
* Timeline sync
* -------------
* Retrieval of events from the timeline depends on the proper batching of
* events. This is computationally expensive to perform on every new event, so
* is deferred by inserting live events to room_$ROOMID_timeline_live. A
* timeline sync reconciles timeline_live and timeline_$INDEX. This involves
* retrieving _live and the highest numbered $INDEX batch. If the batch is < B,
* the earliest entries from _live are inserted into the $INDEX until the
* batch == B. Then, the remaining entries in _live are batched to $INDEX+1,
* $INDEX+2, and so on. The easiest way to visualise this is that the timeline
* goes from old to new, left to right:
* -2 -1 0 1
* <--OLD---------------------------------------NEW-->
* [a,b,c] [d,e,f] [g,h,i] [j,k,l]
*
* Purging
* -------
* Events from the timeline can be purged by removing the lowest
* timeline_$INDEX in the store.
*
* Example
* -------
* A room with room_id !foo:bar has 9 messages (M1->9 where 9=newest) with a
* batch size of 4. The very first time, there is no entry for !foo:bar until
* storeRoom() is called, which results in the keys: [Full Insert]
* room_!foo:bar_timeline_0 : [M1, M2, M3, M4]
* room_!foo:bar_timeline_1 : [M5, M6, M7, M8]
* room_!foo:bar_timeline_2 : [M9]
* room_!foo:bar_state: { ... }
*
* 5 new messages (N1-5, 5=newest) arrive and are then added: [Incremental Insert]
* room_!foo:bar_timeline_live: [N1]
* room_!foo:bar_timeline_live: [N1, N2]
* room_!foo:bar_timeline_live: [N1, N2, N3]
* room_!foo:bar_timeline_live: [N1, N2, N3, N4]
* room_!foo:bar_timeline_live: [N1, N2, N3, N4, N5]
*
* App is shutdown. Restarts. The timeline is synced [Timeline Sync]
* room_!foo:bar_timeline_2 : [M9, N1, N2, N3]
* room_!foo:bar_timeline_3 : [N4, N5]
* room_!foo:bar_timeline_live: []
*
* And the room is retrieved with 8 messages: [Room Retrieval]
* Room.timeline: [M7, M8, M9, N1, N2, N3, N4, N5]
* Room.storageToken: => early_index = 1 because that's where M7 is.
*
* 3 earlier messages are requested: [Earlier retrieval]
* Use storageToken to find batch index 1. Scan batch for earliest event ID.
* earliest event = M7
* events = room_!foo:bar_timeline_1 where event < M7 = [M5, M6]
* Too few events, use next index (0) and get 1 more:
* events = room_!foo:bar_timeline_0 = [M1, M2, M3, M4] => [M4]
* Return concatentation:
* [M4, M5, M6]
*
* Purge oldest events: [Purge]
* del room_!foo:bar_timeline_0
* </pre>
* @module store/webstorage
*/
var DEBUG = false; // set true to enable console logging.
var utils = require("../utils");
var Room = require("../models/room");
var User = require("../models/user");
var MatrixEvent = require("../models/event").MatrixEvent;
/**
* Construct a web storage store, capable of storing rooms and users.
* @constructor
* @param {WebStorage} webStore A web storage implementation, e.g.
* 'window.localStorage' or 'window.sessionStorage' or a custom implementation.
* @param {integer} batchSize The number of events to store per key/value (room
* scoped). Use -1 to store all events for a room under one key/value.
* @throws if the supplied 'store' does not meet the Storage interface of the
* WebStorage API.
*/
function WebStorageStore(webStore, batchSize) {
this.store = webStore;
this.batchSize = batchSize;
if (!utils.isFunction(webStore.getItem) || !utils.isFunction(webStore.setItem) ||
!utils.isFunction(webStore.removeItem) || !utils.isFunction(webStore.key)) {
throw new Error(
"Supplied webStore does not meet the WebStorage API interface"
);
}
if (!parseInt(webStore.length) && webStore.length !== 0) {
throw new Error(
"Supplied webStore does not meet the WebStorage API interface (length)"
);
}
// cached list of room_ids this is storing.
this._roomIds = [];
this._syncedWithStore = false;
// tokens used to remember which index the room instance is at.
this._tokens = [
// { earliestIndex: -4 }
];
}
/**
* Retrieve the token to stream from.
* @return {string} The token or null.
*/
WebStorageStore.prototype.getSyncToken = function() {
return this.store.getItem("sync_token");
};
/**
* Set the token to stream from.
* @param {string} token The token to stream from.
*/
WebStorageStore.prototype.setSyncToken = function(token) {
this.store.setItem("sync_token", token);
};
/**
* Store a room in web storage.
* @param {Room} room
*/
WebStorageStore.prototype.storeRoom = function(room) {
var serRoom = SerialisedRoom.fromRoom(room, this.batchSize);
persist(this.store, serRoom);
if (this._roomIds.indexOf(room.roomId) === -1) {
this._roomIds.push(room.roomId);
}
};
/**
* Retrieve a room from web storage.
* @param {string} roomId
* @return {?Room}
*/
WebStorageStore.prototype.getRoom = function(roomId) {
// probe if room exists; break early if not. Every room should have state.
if (!getItem(this.store, keyName(roomId, "state"))) {
debuglog("getRoom: No room with id %s found.", roomId);
return null;
}
var timelineKeys = getTimelineIndices(this.store, roomId);
if (timelineKeys.indexOf("live") !== -1) {
debuglog("getRoom: Live events found. Syncing timeline for %s", roomId);
this._syncTimeline(roomId, timelineKeys);
}
return loadRoom(this.store, roomId, this.batchSize, this._tokens);
};
/**
* Get a list of all rooms from web storage.
* @return {Array} An empty array.
*/
WebStorageStore.prototype.getRooms = function() {
var rooms = [];
var i;
if (!this._syncedWithStore) {
// sync with the store to set this._roomIds correctly. We know there is
// exactly one 'state' key for each room, so we grab them.
this._roomIds = [];
for (i = 0; i < this.store.length; i++) {
if (this.store.key(i).indexOf("room_") === 0 &&
this.store.key(i).indexOf("_state") !== -1) {
// grab the middle bit which is the room ID
var k = this.store.key(i);
this._roomIds.push(
k.substring("room_".length, k.length - "_state".length)
);
}
}
this._syncedWithStore = true;
}
// call getRoom on each room_id
for (i = 0; i < this._roomIds.length; i++) {
var rm = this.getRoom(this._roomIds[i]);
if (rm) {
rooms.push(rm);
}
}
return rooms;
};
/**
* Get a list of summaries from web storage.
* @return {Array} An empty array.
*/
WebStorageStore.prototype.getRoomSummaries = function() {
return [];
};
/**
* Store a user in web storage.
* @param {User} user
*/
WebStorageStore.prototype.storeUser = function(user) {
// persist the events used to make the user, we can reconstruct on demand.
setItem(this.store, "user_" + user.userId, {
presence: user.events.presence ? user.events.presence.event : null
});
};
/**
* Get a user from web storage.
* @param {string} userId
* @return {User}
*/
WebStorageStore.prototype.getUser = function(userId) {
var userData = getItem(this.store, "user_" + userId);
if (!userData) {
return null;
}
var user = new User(userId);
if (userData.presence) {
user.setPresenceEvent(new MatrixEvent(userData.presence));
}
return user;
};
/**
* Retrieve scrollback for this room. Automatically adds events to the timeline.
* @param {Room} room The matrix room to add the events to the start of the timeline.
* @param {integer} limit The max number of old events to retrieve.
* @return {Array<Object>} An array of objects which will be at most 'limit'
* length and at least 0. The objects are the raw event JSON. The last element
* is the 'oldest' (for parity with homeserver scrollback APIs).
*/
WebStorageStore.prototype.scrollback = function(room, limit) {
if (room.storageToken === undefined || room.storageToken >= this._tokens.length) {
return [];
}
// find the index of the earliest event in this room's timeline
var storeData = this._tokens[room.storageToken] || {};
var i;
var earliestIndex = storeData.earliestIndex;
var earliestEventId = room.timeline[0] ? room.timeline[0].getId() : null;
debuglog(
"scrollback in %s (timeline=%s msgs) i=%s, timeline[0].id=%s - req %s events",
room.roomId, room.timeline.length, earliestIndex, earliestEventId, limit
);
var batch = getItem(
this.store, keyName(room.roomId, "timeline", earliestIndex)
);
if (!batch) {
// bad room or already at start, either way we have nothing to give.
debuglog("No batch with index %s found.", earliestIndex);
return [];
}
// populate from this batch first
var scrollback = [];
var foundEventId = false;
for (i = batch.length - 1; i >= 0; i--) {
// go back and find the earliest event ID, THEN start adding entries.
// Make a MatrixEvent so we don't assume .event_id exists
// (e.g v2/v3 JSON may be different)
var matrixEvent = new MatrixEvent(batch[i]);
if (matrixEvent.getId() === earliestEventId) {
foundEventId = true;
debuglog(
"Found timeline[0] event at position %s in batch %s",
i, earliestIndex
);
continue;
}
if (!foundEventId) {
continue;
}
// add entry
debuglog("Add event at position %s in batch %s", i, earliestIndex);
scrollback.push(batch[i]);
if (scrollback.length === limit) {
break;
}
}
if (scrollback.length === limit) {
debuglog("Batch has enough events to satisfy request.");
return scrollback;
}
if (!foundEventId) {
// the earliest index batch didn't contain the event. In other words,
// this timeline is at a state we don't know, so bail.
debuglog(
"Failed to find event ID %s in batch %s", earliestEventId, earliestIndex
);
return [];
}
// get the requested earlier events from earlier batches
while (scrollback.length < limit) {
earliestIndex--;
batch = getItem(
this.store, keyName(room.roomId, "timeline", earliestIndex)
);
if (!batch) {
// no more events
debuglog("No batch found at index %s", earliestIndex);
break;
}
for (i = batch.length - 1; i >= 0; i--) {
debuglog("Add event at position %s in batch %s", i, earliestIndex);
scrollback.push(batch[i]);
if (scrollback.length === limit) {
break;
}
}
}
debuglog(
"Out of %s requested events, returning %s. New index=%s",
limit, scrollback.length, earliestIndex
);
room.addEventsToTimeline(utils.map(scrollback, function(e) {
return new MatrixEvent(e);
}), true);
this._tokens[room.storageToken] = {
earliestIndex: earliestIndex
};
return scrollback;
};
/**
* Store events for a room. The events have already been added to the timeline.
* @param {Room} room The room to store events for.
* @param {Array<MatrixEvent>} events The events to store.
* @param {string} token The token associated with these events.
* @param {boolean} toStart True if these are paginated results. The last element
* is the 'oldest' (for parity with homeserver scrollback APIs).
*/
WebStorageStore.prototype.storeEvents = function(room, events, token, toStart) {
if (toStart) {
// add paginated events to lowest batch indexes (can go -ve)
var lowIndex = getIndexExtremity(
getTimelineIndices(this.store, room.roomId), true
);
var i, key, batch;
for (i = 0; i < events.length; i++) { // loop events to be stored
key = keyName(room.roomId, "timeline", lowIndex);
batch = getItem(this.store, key) || [];
while (batch.length < this.batchSize && i < events.length) {
batch.unshift(events[i].event);
i++; // increment to insert next event into this batch
}
i--; // decrement to avoid skipping one (for loop ++s)
setItem(this.store, key, batch);
lowIndex--; // decrement index to get a new batch.
}
}
else {
// dump as live events
var liveEvents = getItem(
this.store, keyName(room.roomId, "timeline", "live")
) || [];
debuglog(
"Adding %s events to %s live list (which has %s already)",
events.length, room.roomId, liveEvents.length
);
var updateState = false;
liveEvents = liveEvents.concat(utils.map(events, function(me) {
// cheeky check to avoid looping twice
if (me.isState()) {
updateState = true;
}
return me.event;
}));
setItem(
this.store, keyName(room.roomId, "timeline", "live"), liveEvents
);
if (updateState) {
debuglog("Storing state for %s as new events updated state", room.roomId);
// use 0 batch size; we don't care about batching right now.
var serRoom = SerialisedRoom.fromRoom(room, 0);
setItem(this.store, keyName(serRoom.roomId, "state"), serRoom.state);
}
}
};
/**
* Sync the 'live' timeline, batching live events according to 'batchSize'.
* @param {string} roomId The room to sync the timeline.
* @param {Array<String>} timelineIndices Optional. The indices in the timeline
* if known already.
*/
WebStorageStore.prototype._syncTimeline = function(roomId, timelineIndices) {
timelineIndices = timelineIndices || getTimelineIndices(this.store, roomId);
var liveEvents = getItem(this.store, keyName(roomId, "timeline", "live")) || [];
// get the highest numbered $INDEX batch
var highestIndex = getIndexExtremity(timelineIndices);
var hiKey = keyName(roomId, "timeline", highestIndex);
var hiBatch = getItem(this.store, hiKey) || [];
// fill up the existing batch first.
while (hiBatch.length < this.batchSize && liveEvents.length > 0) {
hiBatch.push(liveEvents.shift());
}
setItem(this.store, hiKey, hiBatch);
// start adding new batches as required
var batch = [];
while (liveEvents.length > 0) {
batch.push(liveEvents.shift());
if (batch.length === this.batchSize || liveEvents.length === 0) {
// persist the full batch and make another
highestIndex++;
hiKey = keyName(roomId, "timeline", highestIndex);
setItem(this.store, hiKey, batch);
batch = [];
}
}
// reset live array
setItem(this.store, keyName(roomId, "timeline", "live"), []);
};
function SerialisedRoom(roomId) {
this.state = {
events: {}
};
this.timeline = {
// $INDEX: []
};
this.roomId = roomId;
}
/**
* Convert a Room instance into a SerialisedRoom instance which can be stored
* in the key value store.
* @param {Room} room The matrix room to convert
* @param {integer} batchSize The number of events per timeline batch
* @return {SerialisedRoom} A serialised room representation of 'room'.
*/
SerialisedRoom.fromRoom = function(room, batchSize) {
var self = new SerialisedRoom(room.roomId);
var index;
self.state.pagination_token = room.oldState.paginationToken;
// [room_$ROOMID_state] downcast to POJO from MatrixEvent
utils.forEach(utils.keys(room.currentState.events), function(eventType) {
utils.forEach(utils.keys(room.currentState.events[eventType]), function(skey) {
if (!self.state.events[eventType]) {
self.state.events[eventType] = {};
}
self.state.events[eventType][skey] = (
room.currentState.events[eventType][skey].event
);
});
});
// [room_$ROOMID_timeline_$INDEX]
if (batchSize > 0) {
index = 0;
while (index * batchSize < room.timeline.length) {
self.timeline[index] = room.timeline.slice(
index * batchSize, (index + 1) * batchSize
);
self.timeline[index] = utils.map(self.timeline[index], function(me) {
// use POJO not MatrixEvent
return me.event;
});
index++;
}
}
else { // don't batch
self.timeline[0] = utils.map(room.timeline, function(matrixEvent) {
return matrixEvent.event;
});
}
return self;
};
function loadRoom(store, roomId, numEvents, tokenArray) {
var room = new Room(roomId, tokenArray.length);
// populate state (flatten nested struct to event array)
var currentStateMap = getItem(store, keyName(roomId, "state"));
var stateEvents = [];
utils.forEach(utils.keys(currentStateMap.events), function(eventType) {
utils.forEach(utils.keys(currentStateMap.events[eventType]), function(skey) {
stateEvents.push(currentStateMap.events[eventType][skey]);
});
});
// TODO: Fix logic dupe with MatrixClient._processRoomEvents
var oldStateEvents = utils.map(
utils.deepCopy(stateEvents), function(e) {
return new MatrixEvent(e);
}
);
var currentStateEvents = utils.map(stateEvents, function(e) {
return new MatrixEvent(e);
}
);
room.oldState.setStateEvents(oldStateEvents);
room.currentState.setStateEvents(currentStateEvents);
// add most recent numEvents
var recentEvents = [];
var index = getIndexExtremity(getTimelineIndices(store, roomId));
var eventIndex = index;
var i, key, batch;
while (recentEvents.length < numEvents) {
key = keyName(roomId, "timeline", index);
batch = getItem(store, key) || [];
if (batch.length === 0) {
// nothing left in the store.
break;
}
for (i = batch.length - 1; i >= 0; i--) {
recentEvents.unshift(new MatrixEvent(batch[i]));
if (recentEvents.length === numEvents) {
eventIndex = index;
break;
}
}
index--;
}
// add events backwards to diverge old state correctly.
room.addEventsToTimeline(recentEvents.reverse(), true);
room.oldState.paginationToken = currentStateMap.pagination_token;
// set the token data to let us know which index this room instance is at
// for scrollback.
tokenArray.push({
earliestIndex: eventIndex
});
return room;
}
function persist(store, serRoom) {
setItem(store, keyName(serRoom.roomId, "state"), serRoom.state);
utils.forEach(utils.keys(serRoom.timeline), function(index) {
setItem(store,
keyName(serRoom.roomId, "timeline", index),
serRoom.timeline[index]
);
});
}
function getTimelineIndices(store, roomId) {
var keys = [];
for (var i = 0; i < store.length; i++) {
if (store.key(i).indexOf(keyName(roomId, "timeline_")) !== -1) {
// e.g. room_$ROOMID_timeline_0 => 0
keys.push(
store.key(i).replace(keyName(roomId, "timeline_"), "")
);
}
}
return keys;
}
function getIndexExtremity(timelineIndices, getLowest) {
var extremity, index;
for (var i = 0; i < timelineIndices.length; i++) {
index = parseInt(timelineIndices[i]);
if (!isNaN(index) && (
extremity === undefined ||
!getLowest && index > extremity ||
getLowest && index < extremity)) {
extremity = index;
}
}
return extremity;
}
function keyName(roomId, key, index) {
return "room_" + roomId + "_" + key + (
index === undefined ? "" : ("_" + index)
);
}
function getItem(store, key) {
try {
return JSON.parse(store.getItem(key));
}
catch (e) {
debuglog("Failed to get key %s: %s", key, e);
debuglog(e.stack);
}
return null;
}
function setItem(store, key, val) {
store.setItem(key, JSON.stringify(val));
}
function debuglog() {
if (DEBUG) {
console.log.apply(console, arguments);
}
}
/*
function delRoomStruct(store, roomId) {
var prefix = "room_" + roomId;
var keysToRemove = [];
for (var i = 0; i < store.length; i++) {
if (store.key(i).indexOf(prefix) !== -1) {
keysToRemove.push(store.key(i));
}
}
utils.forEach(keysToRemove, function(key) {
store.removeItem(key);
});
} */
/** Web Storage Store class. */
module.exports = WebStorageStore;
+5 -16
View File
@@ -148,6 +148,7 @@ module.exports.findElement = function(array, fn, reverse) {
* function signature <code>fn(element, index, array)</code>. Return true to
* remove this element and break.
* @param {boolean} reverse True to search in reverse order.
* @return {boolean} True if an element was removed.
*/
module.exports.removeElement = function(array, fn, reverse) {
var i;
@@ -155,17 +156,19 @@ module.exports.removeElement = function(array, fn, reverse) {
for (i = array.length - 1; i >= 0; i--) {
if (fn(array[i], i, array)) {
array.splice(i, 1);
return; }
return true;
}
}
}
else {
for (i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
array.splice(i, 1);
return;
return true;
}
}
}
return false;
};
/**
@@ -215,20 +218,6 @@ module.exports.checkObjectHasNoAdditionalKeys = function(obj, allowedKeys) {
}
};
/**
* Assigns all the properties in src to dst. If these properties are Objects,
* then both src and dst will refer to the same thing.
* @param {Object} src The object to copy properties from.
* @param {Object} dst The object to write properties to.
*/
module.exports.shallowCopy = function(src, dst) {
for (var i in src) {
if (src.hasOwnProperty(i)) {
dst[i] = src[i];
}
}
};
/**
* Deep copy the given object. The object MUST NOT have circular references and
* MUST NOT have functions.
+931
View File
@@ -0,0 +1,931 @@
"use strict";
/**
* This is an internal module. See {@link createNewMatrixCall} for the public API.
* @module webrtc/call
*/
var utils = require("../utils");
var EventEmitter = require("events").EventEmitter;
var DEBUG = true; // set true to enable console logging.
// events: hangup, error(err), replaced(call), state(state, oldState)
/**
* 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 {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.
*/
function MatrixCall(opts) {
this.roomId = opts.roomId;
this.client = opts.client;
this.webRtc = opts.webRtc;
this.URL = opts.URL;
// Array of Objects with urls, username, credential keys
this.turnServers = opts.turnServers || [];
if (this.turnServers.length === 0) {
this.turnServers.push({
urls: [MatrixCall.FALLBACK_STUN_SERVER]
});
}
utils.forEach(this.turnServers, function(server) {
utils.checkObjectHasKeys(server, ["urls"]);
});
this.callId = "c" + new Date().getTime();
this.state = 'fledgling';
this.didConnect = false;
// A queue for candidates waiting to go out.
// We try to amalgamate candidates into a single candidate message where
// possible
this.candidateSendQueue = [];
this.candidateSendTries = 0;
}
/** The length of time a call can be ringing for. */
MatrixCall.CALL_TIMEOUT_MS = 60000;
/** The fallback server to use for STUN. */
MatrixCall.FALLBACK_STUN_SERVER = 'stun:stun.l.google.com:19302';
/** An error code when the local client failed to create an offer. */
MatrixCall.ERR_LOCAL_OFFER_FAILED = "local_offer_failed";
/**
* An error code when there is no local mic/camera to use. This may be because
* the hardware isn't plugged in, or the user has explicitly denied access.
*/
MatrixCall.ERR_NO_USER_MEDIA = "no_user_media";
utils.inherits(MatrixCall, EventEmitter);
/**
* Place a voice call to this room.
* @throws If you have not specified a listener for 'error' events.
*/
MatrixCall.prototype.placeVoiceCall = function() {
checkForErrorListener(this);
_placeCallWithConstraints(this, _getUserMediaVideoContraints('voice'));
this.type = 'voice';
};
/**
* Place a video call to this room.
* @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
* to render the local camera preview.
* @throws If you have not specified a listener for 'error' events.
*/
MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoElement) {
checkForErrorListener(this);
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
_placeCallWithConstraints(this, _getUserMediaVideoContraints('video'));
this.type = 'video';
_tryPlayRemoteStream(this);
};
/**
* Retrieve the local <code>&lt;video&gt;</code> DOM element.
* @return {Element} The dom element
*/
MatrixCall.prototype.getLocalVideoElement = function() {
return this.localVideoElement;
};
/**
* Retrieve the remote <code>&lt;video&gt;</code> DOM element.
* @return {Element} The dom element
*/
MatrixCall.prototype.getRemoteVideoElement = function() {
return this.remoteVideoElement;
};
/**
* Set the local <code>&lt;video&gt;</code> DOM element. If this call is active,
* video will be rendered to it immediately.
* @param {Element} element The <code>&lt;video&gt;</code> DOM element.
*/
MatrixCall.prototype.setLocalVideoElement = function(element) {
this.localVideoElement = element;
if (element && this.localAVStream && this.type === 'video') {
element.autoplay = true;
element.src = this.URL.createObjectURL(this.localAVStream);
element.muted = true;
var self = this;
setTimeout(function() {
var vel = self.getLocalVideoElement();
if (vel.play) {
vel.play();
}
}, 0);
}
};
/**
* Set the remote <code>&lt;video&gt;</code> DOM element. If this call is active,
* video will be rendered to it immediately.
* @param {Element} element The <code>&lt;video&gt;</code> DOM element.
*/
MatrixCall.prototype.setRemoteVideoElement = function(element) {
this.remoteVideoElement = element;
_tryPlayRemoteStream(this);
};
/**
* Configure this call from an invite event. Used by MatrixClient.
* @protected
* @param {MatrixEvent} event The m.call.invite event
*/
MatrixCall.prototype._initWithInvite = function(event) {
this.msg = event.getContent();
this.peerConn = _createPeerConnection(this);
var self = this;
if (this.peerConn) {
this.peerConn.setRemoteDescription(
new this.webRtc.RtcSessionDescription(this.msg.offer),
hookCallback(self, self._onSetRemoteDescriptionSuccess),
hookCallback(self, self._onSetRemoteDescriptionError)
);
}
setState(this, 'ringing');
this.direction = 'inbound';
// firefox and OpenWebRTC's RTCPeerConnection doesn't add streams until it
// starts getting media on them so we need to figure out whether a video
// channel has been offered by ourselves.
if (this.msg.offer.sdp.indexOf('m=video') > -1) {
this.type = 'video';
}
else {
this.type = 'voice';
}
if (event.getAge()) {
setTimeout(function() {
if (self.state == 'ringing') {
self.hangupParty = 'remote'; // effectively
setState(self, 'ended');
stopAllMedia(self);
if (self.peerConn.signalingState != 'closed') {
self.peerConn.close();
}
self.emit("hangup", self);
}
}, this.msg.lifetime - event.getAge());
}
};
/**
* Configure this call from a hangup event. Used by MatrixClient.
* @protected
* @param {MatrixEvent} event The m.call.hangup event
*/
MatrixCall.prototype._initWithHangup = function(event) {
// perverse as it may seem, sometimes we want to instantiate a call with a
// hangup message (because when getting the state of the room on load, events
// come in reverse order and we want to remember that a call has been hung up)
this.msg = event.getContent();
setState(this, 'ended');
};
/**
* Answer a call.
*/
MatrixCall.prototype.answer = function() {
debuglog("Answering call %s of type %s", this.callId, this.type);
var self = this;
if (!this.localAVStream && !this.waitForLocalAVStream) {
this.webRtc.getUserMedia(
_getUserMediaVideoContraints(this.type),
hookCallback(self, self._gotUserMediaForAnswer),
hookCallback(self, self._getUserMediaFailed)
);
setState(this, 'wait_local_media');
} else if (this.localAVStream) {
this._gotUserMediaForAnswer(this.localAVStream);
} else if (this.waitForLocalAVStream) {
setState(this, 'wait_local_media');
}
};
/**
* Replace this call with a new call, e.g. for glare resolution. Used by
* MatrixClient.
* @protected
* @param {MatrixCall} newCall The new call.
*/
MatrixCall.prototype._replacedBy = function(newCall) {
debuglog(this.callId + " being replaced by " + newCall.callId);
if (this.state == 'wait_local_media') {
debuglog("Telling new call to wait for local media");
newCall.waitForLocalAVStream = true;
} else if (this.state == 'create_offer') {
debuglog("Handing local stream to new call");
newCall._gotUserMediaForAnswer(this.localAVStream);
delete(this.localAVStream);
} else if (this.state == 'invite_sent') {
debuglog("Handing local stream to new call");
newCall._gotUserMediaForAnswer(this.localAVStream);
delete(this.localAVStream);
}
newCall.localVideoElement = this.localVideoElement;
newCall.remoteVideoElement = this.remoteVideoElement;
this.successor = newCall;
this.emit("replaced", newCall);
this.hangup(true);
};
/**
* Hangup a call.
* @param {string} reason The reason why the call is being hung up.
* @param {boolean} suppressEvent True to suppress emitting an event.
*/
MatrixCall.prototype.hangup = function(reason, suppressEvent) {
debuglog("Ending call " + this.callId);
terminate(this, "local", reason, !suppressEvent);
var content = {
version: 0,
call_id: this.callId,
reason: reason
};
sendEvent(this, 'm.call.hangup', content);
};
/**
* Internal
* @private
* @param {Object} stream
*/
MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
if (this.successor) {
this.successor._gotUserMediaForAnswer(stream);
return;
}
if (this.state == 'ended') {
return;
}
var self = this;
var videoEl = this.getLocalVideoElement();
if (videoEl && this.type == 'video') {
videoEl.autoplay = true;
videoEl.src = this.URL.createObjectURL(stream);
videoEl.muted = true;
setTimeout(function() {
var vel = self.getLocalVideoElement();
if (vel.play) {
vel.play();
}
}, 0);
}
this.localAVStream = stream;
var audioTracks = stream.getAudioTracks();
for (var i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = true;
}
this.peerConn = _createPeerConnection(this);
this.peerConn.addStream(stream);
this.peerConn.createOffer(
hookCallback(self, self._gotLocalOffer),
hookCallback(self, self._getLocalOfferFailed)
);
setState(self, 'create_offer');
};
/**
* Internal
* @private
* @param {Object} stream
*/
MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
var self = this;
if (self.state == 'ended') {
return;
}
var localVidEl = self.getLocalVideoElement();
if (localVidEl && self.type == 'video') {
localVidEl.autoplay = true;
localVidEl.src = self.URL.createObjectURL(stream);
localVidEl.muted = true;
setTimeout(function() {
var vel = self.getLocalVideoElement();
if (vel.play) {
vel.play();
}
}, 0);
}
self.localAVStream = stream;
var audioTracks = stream.getAudioTracks();
for (var i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = true;
}
self.peerConn.addStream(stream);
var constraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': self.type == 'video'
}
};
self.peerConn.createAnswer(function(description) {
debuglog("Created answer: " + description);
self.peerConn.setLocalDescription(description, function() {
var content = {
version: 0,
call_id: self.callId,
answer: {
sdp: self.peerConn.localDescription.sdp,
type: self.peerConn.localDescription.type
}
};
sendEvent(self, 'm.call.answer', content);
setState(self, 'connecting');
}, function() {
debuglog("Error setting local description!");
}, constraints);
}, function(err) {
debuglog("Failed to create answer: " + err);
});
setState(self, 'create_answer');
};
/**
* Internal
* @private
* @param {Object} event
*/
MatrixCall.prototype._gotLocalIceCandidate = function(event) {
if (event.candidate) {
debuglog(
"Got local ICE " + event.candidate.sdpMid + " candidate: " +
event.candidate.candidate
);
// As with the offer, note we need to make a copy of this object, not
// pass the original: that broke in Chrome ~m43.
var c = {
candidate: event.candidate.candidate,
sdpMid: event.candidate.sdpMid,
sdpMLineIndex: event.candidate.sdpMLineIndex
};
sendCandidate(this, c);
}
};
/**
* Used by MatrixClient.
* @protected
* @param {Object} cand
*/
MatrixCall.prototype._gotRemoteIceCandidate = function(cand) {
if (this.state == 'ended') {
//debuglog("Ignoring remote ICE candidate because call has ended");
return;
}
debuglog("Got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
this.peerConn.addIceCandidate(
new this.webRtc.RtcIceCandidate(cand),
function() {},
function(e) {}
);
};
/**
* Used by MatrixClient.
* @protected
* @param {Object} msg
*/
MatrixCall.prototype._receivedAnswer = function(msg) {
if (this.state == 'ended') {
return;
}
var self = this;
this.peerConn.setRemoteDescription(
new this.webRtc.RtcSessionDescription(msg.answer),
hookCallback(self, self._onSetRemoteDescriptionSuccess),
hookCallback(self, self._onSetRemoteDescriptionError)
);
setState(self, 'connecting');
};
/**
* Internal
* @private
* @param {Object} description
*/
MatrixCall.prototype._gotLocalOffer = function(description) {
var self = this;
debuglog("Created offer: " + description);
if (self.state == 'ended') {
debuglog("Ignoring newly created offer on call ID " + self.callId +
" because the call has ended");
return;
}
self.peerConn.setLocalDescription(description, function() {
var content = {
version: 0,
call_id: self.callId,
// OpenWebRTC appears to add extra stuff (like the DTLS fingerprint)
// to the description when setting it on the peerconnection.
// According to the spec it should only add ICE
// candidates. Any ICE candidates that have already been generated
// at this point will probably be sent both in the offer and separately.
// Also, note that we have to make a new object here, copying the
// type and sdp properties.
// Passing the RTCSessionDescription object as-is doesn't work in
// Chrome (as of about m43).
offer: {
sdp: self.peerConn.localDescription.sdp,
type: self.peerConn.localDescription.type
},
lifetime: MatrixCall.CALL_TIMEOUT_MS
};
sendEvent(self, 'm.call.invite', content);
setTimeout(function() {
if (self.state == 'invite_sent') {
self.hangup('invite_timeout');
}
}, MatrixCall.CALL_TIMEOUT_MS);
setState(self, 'invite_sent');
}, function() {
debuglog("Error setting local description!");
});
};
/**
* Internal
* @private
* @param {Object} error
*/
MatrixCall.prototype._getLocalOfferFailed = function(error) {
this.emit(
"error",
callError(MatrixCall.ERR_LOCAL_OFFER_FAILED, "Failed to start audio for call!")
);
};
/**
* Internal
* @private
*/
MatrixCall.prototype._getUserMediaFailed = function() {
this.emit(
"error",
callError(
MatrixCall.ERR_NO_USER_MEDIA,
"Couldn't start capturing media! Is your microphone set up and " +
"does this app have permission?"
)
);
this.hangup("user_media_failed");
};
/**
* Internal
* @private
*/
MatrixCall.prototype._onIceConnectionStateChanged = function() {
if (this.state == 'ended') {
return; // because ICE can still complete as we're ending the call
}
debuglog(
"Ice connection state changed to: " + this.peerConn.iceConnectionState
);
// ideally we'd consider the call to be connected when we get media but
// chrome doesn't implement any of the 'onstarted' events yet
if (this.peerConn.iceConnectionState == 'completed' ||
this.peerConn.iceConnectionState == 'connected') {
setState(this, 'connected');
this.didConnect = true;
} else if (this.peerConn.iceConnectionState == 'failed') {
this.hangup('ice_failed');
}
};
/**
* Internal
* @private
*/
MatrixCall.prototype._onSignallingStateChanged = function() {
debuglog(
"call " + this.callId + ": Signalling state changed to: " +
this.peerConn.signalingState
);
};
/**
* Internal
* @private
*/
MatrixCall.prototype._onSetRemoteDescriptionSuccess = function() {
debuglog("Set remote description");
};
/**
* Internal
* @private
* @param {Object} e
*/
MatrixCall.prototype._onSetRemoteDescriptionError = function(e) {
debuglog("Failed to set remote description" + e);
};
/**
* Internal
* @private
* @param {Object} event
*/
MatrixCall.prototype._onAddStream = function(event) {
debuglog("Stream added" + event);
var s = event.stream;
this.remoteAVStream = s;
if (this.direction == 'inbound') {
if (s.getVideoTracks().length > 0) {
this.type = 'video';
} else {
this.type = 'voice';
}
}
var self = this;
forAllTracksOnStream(s, function(t) {
// not currently implemented in chrome
t.onstarted = hookCallback(self, self._onRemoteStreamTrackStarted);
});
event.stream.onended = hookCallback(self, self._onRemoteStreamEnded);
// not currently implemented in chrome
event.stream.onstarted = hookCallback(self, self._onRemoteStreamStarted);
_tryPlayRemoteStream(this);
};
/**
* Internal
* @private
* @param {Object} event
*/
MatrixCall.prototype._onRemoteStreamStarted = function(event) {
setState(this, 'connected');
};
/**
* Internal
* @private
* @param {Object} event
*/
MatrixCall.prototype._onRemoteStreamEnded = function(event) {
debuglog("Remote stream ended");
this.hangupParty = 'remote';
setState(this, 'ended');
stopAllMedia(this);
if (this.peerConn.signalingState != 'closed') {
this.peerConn.close();
}
this.emit("hangup", this);
};
/**
* Internal
* @private
* @param {Object} event
*/
MatrixCall.prototype._onRemoteStreamTrackStarted = function(event) {
setState(this, 'connected');
};
/**
* Used by MatrixClient.
* @protected
* @param {Object} msg
*/
MatrixCall.prototype._onHangupReceived = function(msg) {
debuglog("Hangup received");
terminate(this, "remote", msg.reason, true);
};
/**
* Used by MatrixClient.
* @protected
* @param {Object} msg
*/
MatrixCall.prototype._onAnsweredElsewhere = function(msg) {
debuglog("Answered elsewhere");
terminate(this, "remote", "answered_elsewhere", true);
};
var setState = function(self, state) {
var oldState = self.state;
self.state = state;
self.emit("state", state, oldState);
};
/**
* Internal
* @param {MatrixCall} self
* @param {string} eventType
* @param {Object} content
* @return {Promise}
*/
var sendEvent = function(self, eventType, content) {
return self.client.sendEvent(self.roomId, eventType, content);
};
var 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);
if (self.candidateSendTries === 0) {
setTimeout(function() {
_sendCandidateQueue(self);
}, 100);
}
};
var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
if (self.getRemoteVideoElement()) {
if (self.getRemoteVideoElement().pause) {
self.getRemoteVideoElement().pause();
}
self.getRemoteVideoElement().src = "";
}
if (self.getLocalVideoElement()) {
if (self.getLocalVideoElement().pause) {
self.getLocalVideoElement().pause();
}
self.getLocalVideoElement().src = "";
}
self.hangupParty = hangupParty;
self.hangupReason = hangupReason;
setState(self, 'ended');
stopAllMedia(self);
if (self.peerConn && self.peerConn.signalingState !== 'closed') {
self.peerConn.close();
}
if (shouldEmit) {
self.emit("hangup", self);
}
};
var stopAllMedia = function(self) {
if (self.localAVStream) {
forAllTracksOnStream(self.localAVStream, function(t) {
if (t.stop) {
t.stop();
}
});
// also call stop on the main stream so firefox will stop sharing
// the mic
if (self.localAVStream.stop) {
self.localAVStream.stop();
}
}
if (self.remoteAVStream) {
forAllTracksOnStream(self.remoteAVStream, function(t) {
if (t.stop) {
t.stop();
}
});
}
};
var _tryPlayRemoteStream = function(self) {
if (self.getRemoteVideoElement() && self.remoteAVStream) {
var player = self.getRemoteVideoElement();
player.autoplay = true;
player.src = self.URL.createObjectURL(self.remoteAVStream);
setTimeout(function() {
var vel = self.getRemoteVideoElement();
if (vel.play) {
vel.play();
}
// OpenWebRTC does not support oniceconnectionstatechange yet
if (self.webRtc.isOpenWebRTC()) {
setState(self, 'connected');
}
}, 0);
}
};
var checkForErrorListener = function(self) {
if (self.listeners("error").length === 0) {
throw new Error(
"You MUST attach an error listener using call.on('error', function() {})"
);
}
};
var callError = function(code, msg) {
var e = new Error(msg);
e.code = code;
return e;
};
var debuglog = function() {
if (DEBUG) {
console.log.apply(console, arguments);
}
};
var _sendCandidateQueue = function(self) {
if (self.candidateSendQueue.length === 0) {
return;
}
var cands = self.candidateSendQueue;
self.candidateSendQueue = [];
++self.candidateSendTries;
var content = {
version: 0,
call_id: self.callId,
candidates: cands
};
debuglog("Attempting to send " + cands.length + " candidates");
sendEvent(self, 'm.call.candidates', content).then(function() {
self.candidateSendTries = 0;
_sendCandidateQueue(self);
}, function(error) {
for (var i = 0; i < cands.length; i++) {
self.candidateSendQueue.push(cands[i]);
}
if (self.candidateSendTries > 5) {
debuglog(
"Failed to send candidates on attempt %s. Giving up for now.",
self.candidateSendTries
);
self.candidateSendTries = 0;
return;
}
var delayMs = 500 * Math.pow(2, self.candidateSendTries);
++self.candidateSendTries;
debuglog("Failed to send candidates. Retrying in " + delayMs + "ms");
setTimeout(function() {
_sendCandidateQueue(self);
}, delayMs);
});
};
var _placeCallWithConstraints = function(self, constraints) {
self.client.callList[self.callId] = self;
self.webRtc.getUserMedia(
constraints,
hookCallback(self, self._gotUserMediaForInvite),
hookCallback(self, self._getUserMediaFailed)
);
setState(self, 'wait_local_media');
self.direction = 'outbound';
self.config = constraints;
};
var _createPeerConnection = function(self) {
var servers = self.turnServers;
if (self.webRtc.vendor === "mozilla") {
// modify turnServers struct to match what mozilla expects.
servers = [];
for (var i = 0; i < self.turnServers.length; i++) {
for (var j = 0; j < self.turnServers[i].urls.length; j++) {
servers.push({
url: self.turnServers[i].urls[j],
username: self.turnServers[i].username,
credential: self.turnServers[i].credential
});
}
}
}
var pc = new self.webRtc.RtcPeerConnection({
iceServers: servers
});
pc.oniceconnectionstatechange = hookCallback(self, self._onIceConnectionStateChanged);
pc.onsignalingstatechange = hookCallback(self, self._onSignallingStateChanged);
pc.onicecandidate = hookCallback(self, self._gotLocalIceCandidate);
pc.onaddstream = hookCallback(self, self._onAddStream);
return pc;
};
var _getUserMediaVideoContraints = function(callType) {
switch (callType) {
case 'voice':
return ({audio: true, video: false});
case 'video':
return ({audio: true, video: {
mandatory: {
minWidth: 640,
maxWidth: 640,
minHeight: 360,
maxHeight: 360
}
}});
}
};
var hookCallback = function(call, fn) {
return function() {
return fn.apply(call, arguments);
};
};
var forAllVideoTracksOnStream = function(s, f) {
var tracks = s.getVideoTracks();
for (var i = 0; i < tracks.length; i++) {
f(tracks[i]);
}
};
var forAllAudioTracksOnStream = function(s, f) {
var tracks = s.getAudioTracks();
for (var i = 0; i < tracks.length; i++) {
f(tracks[i]);
}
};
var forAllTracksOnStream = function(s, f) {
forAllVideoTracksOnStream(s, f);
forAllAudioTracksOnStream(s, f);
};
/** The MatrixCall class. */
module.exports.MatrixCall = MatrixCall;
/**
* 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.
* @return {MatrixCall} the call or null if the browser doesn't support calling.
*/
module.exports.createNewMatrixCall = function(client, roomId) {
var w = global.window;
var doc = global.document;
if (!w || !doc) {
return null;
}
var webRtc = {};
webRtc.isOpenWebRTC = function() {
var scripts = doc.getElementById("script");
if (!scripts || !scripts.length) {
return false;
}
for (var i = 0; i < scripts.length; i++) {
if (scripts[i].src.indexOf("owr.js") > -1) {
return true;
}
}
return false;
};
var getUserMedia = (
w.navigator.getUserMedia || w.navigator.webkitGetUserMedia ||
w.navigator.mozGetUserMedia
);
if (getUserMedia) {
webRtc.getUserMedia = function() {
return getUserMedia.apply(w.navigator, arguments);
};
}
webRtc.RtcPeerConnection = (
w.RTCPeerConnection || w.webkitRTCPeerConnection || w.mozRTCPeerConnection
);
webRtc.RtcSessionDescription = (
w.RTCSessionDescription || w.webkitRTCSessionDescription ||
w.mozRTCSessionDescription
);
webRtc.RtcIceCandidate = (
w.RTCIceCandidate || w.webkitRTCIceCandidate || w.mozRTCIceCandidate
);
webRtc.vendor = null;
if (w.mozRTCPeerConnection) {
webRtc.vendor = "mozilla";
}
else if (w.webkitRTCPeerConnection) {
webRtc.vendor = "webkit";
}
else if (w.RTCPeerConnection) {
webRtc.vendor = "generic";
}
if (!webRtc.RtcIceCandidate || !webRtc.RtcSessionDescription ||
!webRtc.RtcPeerConnection || !webRtc.getUserMedia) {
return null; // WebRTC is not supported.
}
var opts = {
webRtc: webRtc,
client: client,
URL: w.URL,
roomId: roomId,
turnServers: client.getTurnServers()
};
return new MatrixCall(opts);
};
+5 -4
View File
@@ -1,13 +1,14 @@
{
"name": "matrix-js-sdk",
"version": "0.1.1",
"version": "0.2.0",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test": "istanbul cover --report cobertura -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --forceexit --captureExceptions",
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js",
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --forceexit --captureExceptions",
"check": "jasmine-node spec --verbose --junitreport --forceexit --captureExceptions",
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js --ignore-missing",
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200 --max_line_length 90 -r spec/ -r lib/",
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200,0222 --max_line_length 90 -r spec/ -r lib/",
"release": "npm run build && mkdir dist/$npm_package_version && uglifyjs -c -m -o dist/$npm_package_version/browser-matrix-$npm_package_version.min.js dist/browser-matrix-dev.js && cp dist/browser-matrix-dev.js dist/$npm_package_version/browser-matrix-$npm_package_version.js"
},
"repository": {
+244
View File
@@ -0,0 +1,244 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
function MockStorageApi() {
this.data = {};
}
MockStorageApi.prototype = {
setItem: function(k, v) {
this.data[k] = v;
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
}
};
describe("MatrixClient crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
var baseUrl = "http://localhost.or.something";
var httpBackend;
var aliClient;
var roomId = "!room:localhost";
var aliUserId = "@ali:localhost";
var aliDeviceId = "zxcvb";
var aliAccessToken = "aseukfgwef";
var bobClient;
var bobUserId = "@bob:localhost";
var bobDeviceId = "bvcxz";
var bobAccessToken = "fewgfkuesa";
var bobOneTimeKeys;
var bobDeviceKeys;
var bobDeviceCurve25519Key;
var bobDeviceEd25519Key;
var aliLocalStore;
var aliStorage;
var bobStorage;
var aliMessage;
beforeEach(function() {
aliLocalStore = new MockStorageApi();
aliStorage = new sdk.WebStorageSessionStore(aliLocalStore);
bobStorage = new sdk.WebStorageSessionStore(new MockStorageApi());
utils.beforeEach(this);
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
aliClient = sdk.createClient({
baseUrl: baseUrl,
userId: aliUserId,
accessToken: aliAccessToken,
deviceId: aliDeviceId,
sessionStore: aliStorage
});
bobClient = sdk.createClient({
baseUrl: baseUrl,
userId: bobUserId,
accessToken: bobAccessToken,
deviceId: bobDeviceId,
sessionStore: bobStorage
});
httpBackend.when("GET", "/pushrules").respond(200, {});
});
describe("Ali account setup", function() {
it("should have device keys", function(done) {
expect(aliClient.deviceKeys).toBeDefined();
expect(aliClient.deviceKeys.user_id).toEqual(aliUserId);
expect(aliClient.deviceKeys.device_id).toEqual(aliDeviceId);
done();
});
it("should have a curve25519 key", function(done) {
expect(aliClient.deviceCurve25519Key).toBeDefined();
done();
});
});
function bobUploadsKeys(done) {
var uploadPath = "/keys/upload/bvcxz";
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).toEqual({});
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).not.toEqual({});
bobDeviceKeys = content.device_keys;
bobOneTimeKeys = content.one_time_keys;
var count = 0;
for (var key in content.one_time_keys) {
if (content.one_time_keys.hasOwnProperty(key)) {
count++;
}
}
expect(count).toEqual(5);
return {one_time_key_counts: {curve25519: count}};
});
return {one_time_key_counts: {}};
});
bobClient.uploadKeys(5);
httpBackend.flush().done(function() {
expect(bobDeviceKeys).toBeDefined();
expect(bobOneTimeKeys).toBeDefined();
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
bobDeviceEd25519Key = bobDeviceKeys.keys["ed25519:bvcxz"];
done();
});
}
it("Bob uploads without one-time keys and with one-time keys", bobUploadsKeys);
function aliDownloadsKeys(done) {
var bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
httpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual({});
var result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
});
aliClient.downloadKeys([bobUserId]).then(function() {
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
id: "bvcxz",
key: bobDeviceEd25519Key
}]);
});
httpBackend.flush().done(function() {
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
expect(devices).toEqual(bobKeys);
done();
});
}
it("Ali downloads Bobs keys", function(done) {
bobUploadsKeys(function() {aliDownloadsKeys(done);});
});
function aliEnablesEncryption(done) {
httpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
for (var keyId in bobOneTimeKeys) {
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
if (keyId.indexOf("curve25519:") === 0) {
break;
}
}
}
var result = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
return {one_time_keys: result};
});
aliClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
members: [aliUserId, bobUserId]
}).then(function(res) {
expect(res.missingUsers).toEqual([]);
expect(res.missingDevices).toEqual({});
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
done();
});
httpBackend.flush();
}
it("Ali enables encryption", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(done);
});
});
});
function aliSendsMessage(done) {
var txnId = "a.transaction.id";
var path = "/send/m.room.encrypted/" + txnId;
httpBackend.when("PUT", path).respond(200, function(path, content) {
aliMessage = content;
expect(aliMessage.ciphertext[bobDeviceCurve25519Key]).toBeDefined();
return {};
});
aliClient.sendMessage(
roomId, {msgtype: "m.text", body: "Hello, World"}, txnId
);
httpBackend.flush().done(function() {done();});
}
it("Ali sends a message", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(function() {
aliSendsMessage(done);
});
});
});
});
function bobRecvMessage(done) {
var initialSync = {
end: "alpha",
presence: [],
rooms: []
};
var events = {
start: "alpha",
end: "beta",
chunk: [utils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: aliMessage
})]
};
httpBackend.when("GET", "initialSync").respond(200, initialSync);
httpBackend.when("GET", "events").respond(200, events);
bobClient.on("event", function(event) {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
msgtype: "m.text",
body: "Hello, World"
});
expect(event.isEncrypted()).toBeTruthy();
done();
});
bobClient.startClient();
httpBackend.flush();
}
it("Bob receives a message", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(function() {
aliSendsMessage(function() {
bobRecvMessage(done);
});
});
});
});
}, 30000); //timeout after 30s
});
@@ -3,12 +3,11 @@ var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
describe("MatrixClient", function() {
describe("MatrixClient events", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var selfUserId = "@alice:localhost";
var selfAccessToken = "aseukfgwef";
var otherUserId = "@bob:localhost";
beforeEach(function() {
utils.beforeEach(this);
@@ -19,51 +18,14 @@ describe("MatrixClient", function() {
userId: selfUserId,
accessToken: selfAccessToken
});
httpBackend.when("GET", "/pushrules").respond(200, {});
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
});
describe("startClient", function() {
var initialSync = {
end: "s_5_3",
presence: [],
rooms: []
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: []
};
it("should start with /initialSync then move onto /events.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
done();
});
});
it("should pass the 'end' token from /initialSync to the from= param " +
" of /events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").check(function(req) {
expect(req.queryParams.from).toEqual(initialSync.end);
}).respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
done();
});
});
});
describe("EventEmitter", function() {
describe("emissions", function() {
var initialSync = {
end: "s_5_3",
presence: [{
@@ -82,13 +44,20 @@ describe("MatrixClient", function() {
start: "s",
end: "t",
chunk: [
utils.mkMessage("!erufh:bar", "@foo:bar", "hmmm")
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm"
})
]
},
state: [
utils.mkMembership("!erufh:bar", "join", "@foo:bar"),
utils.mkEvent("m.room.create", "!erufh:bar", "@foo:bar", {
creator: "@foo:bar"
utils.mkMembership({
room: "!erufh:bar", mship: "join", user: "@foo:bar"
}),
utils.mkEvent({
type: "m.room.create", room: "!erufh:bar", user: "@foo:bar",
content: {
creator: "@foo:bar"
}
})
]
}]
@@ -97,10 +66,16 @@ describe("MatrixClient", function() {
start: "s_5_3",
end: "e_6_7",
chunk: [
utils.mkMessage("!erufh:bar", "@foo:bar", "ello ello"),
utils.mkMessage("!erufh:bar", "@foo:bar", ":D"),
utils.mkEvent("m.typing", "!erufh:bar", "bar", {
user_ids: ["@foo:bar"]
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "ello ello"
}),
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: ":D"
}),
utils.mkEvent({
type: "m.typing", room: "!erufh:bar", content: {
user_ids: ["@foo:bar"]
}
})
]
};
@@ -299,140 +274,4 @@ describe("MatrixClient", function() {
});
});
describe("room state", function() {
var roomOne = "!foo:localhost";
var roomTwo = "!bar:localhost";
var msgText = "some text here";
var otherDisplayName = "Bob Smith";
var initialSync = {
end: "s_5_3",
presence: [],
rooms: [
{
membership: "join",
room_id: roomOne,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage(roomOne, otherUserId, "hello")
]
},
state: [
utils.mkEvent(
"m.room.name", roomOne, otherUserId,
{
name: "Old room name"
}
),
utils.mkMembership(roomOne, "join", otherUserId),
utils.mkMembership(roomOne, "join", selfUserId),
utils.mkEvent(
"m.room.create", roomOne, selfUserId,
{
creator: selfUserId
}
)
]
},
{
membership: "join",
room_id: roomTwo,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage(roomTwo, otherUserId, "hiii")
]
},
state: [
utils.mkMembership(
roomTwo, "join", otherUserId, null, otherDisplayName
),
utils.mkMembership(roomTwo, "join", selfUserId),
utils.mkEvent(
"m.room.create", roomTwo, selfUserId,
{
creator: selfUserId
}
)
]
}
]
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: [
utils.mkEvent("m.room.name", roomOne, selfUserId, {
name: "A new room name"
}),
utils.mkMessage(roomTwo, otherUserId, msgText),
utils.mkEvent("m.typing", roomTwo, undefined, {
user_ids: [otherUserId]
})
]
};
it("should continually recalculate the right room name.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomOne);
// should have clobbered the name to the one from /events
expect(room.name).toEqual(eventData.chunk[0].content.name);
done();
});
});
it("should store the right events in the timeline.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
var 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) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
var 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) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomTwo);
var member = room.getMember(otherUserId);
expect(member).toBeDefined();
expect(member.typing).toEqual(true);
member = room.getMember(selfUserId);
expect(member).toBeDefined();
expect(member.typing).toEqual(false);
done();
});
});
});
});
+46
View File
@@ -0,0 +1,46 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var publicGlobals = require("../../lib/matrix");
var Room = publicGlobals.Room;
var MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
var utils = require("../test-utils");
describe("MatrixClient", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend, store;
var userId = "@alice:localhost";
var accessToken = "aseukfgwef";
beforeEach(function() {
utils.beforeEach(this);
httpBackend = new HttpBackend();
store = new MatrixInMemoryStore();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
store: store
});
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
});
describe("joinRoom", function() {
it("should no-op if you've already joined a room", function() {
var roomId = "!foo:bar";
var room = new Room(roomId);
room.addEvents([
utils.mkMembership({
user: userId, room: roomId, mship: "join", event: true
})
]);
store.storeRoom(room);
client.joinRoom(roomId);
httpBackend.verifyNoOutstandingRequests();
});
});
});
+181
View File
@@ -0,0 +1,181 @@
"use strict";
var sdk = require("../..");
var MatrixClient = sdk.MatrixClient;
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
describe("MatrixClient opts", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var userId = "@alice:localhost";
var userB = "@bob:localhost";
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var eventData = {
chunk: [],
start: "s",
end: "e"
};
var initialSync = {
end: "s_5_3",
presence: [],
rooms: [{
membership: "join",
room_id: roomId,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomId, user: userB, msg: "hello"
})
]
},
state: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
content: {
name: "Old room name"
}
}),
utils.mkMembership({
room: roomId, mship: "join", user: userB, name: "Bob"
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: "Alice"
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}]
};
beforeEach(function() {
utils.beforeEach(this);
httpBackend = new HttpBackend();
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
});
describe("without opts.store", function() {
beforeEach(function() {
client = new MatrixClient({
request: httpBackend.requestFn,
store: undefined,
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
scheduler: new sdk.MatrixScheduler()
});
});
it("should be able to send messages", function(done) {
var eventId = "$flibble:wibble";
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
});
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
expect(res.event_id).toEqual(eventId);
done();
});
httpBackend.flush("/txn1", 1);
});
it("should be able to sync / get new events", function(done) {
var 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(
-1, "Recv unexpected event type: " + event.getType()
);
expectedEventTypes.splice(
expectedEventTypes.indexOf(event.getType()), 1
);
});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
client.startClient();
httpBackend.flush("/pushrules", 1).then(function() {
return httpBackend.flush("/initialSync", 1);
}).then(function() {
return httpBackend.flush("/events", 1);
}).done(function() {
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes
);
done();
});
});
});
describe("without opts.scheduler", function() {
beforeEach(function() {
client = new MatrixClient({
request: httpBackend.requestFn,
store: new sdk.MatrixInMemoryStore(),
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
scheduler: undefined
});
});
it("shouldn't retry sending events", function(done) {
httpBackend.when("PUT", "/txn1").fail(500, {
errcode: "M_SOMETHING",
error: "Ruh roh"
});
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
}, function(err) {
expect(err.errcode).toEqual("M_SOMETHING");
done();
});
httpBackend.flush("/txn1", 1);
});
it("shouldn't queue events", function(done) {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "AAA"
});
httpBackend.when("PUT", "/txn2").respond(200, {
event_id: "BBB"
});
var sentA = false;
var sentB = false;
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
sentA = true;
expect(sentB).toBe(true);
});
client.sendTextMessage("!foo:bar", "b body", "txn2").done(function(res) {
sentB = true;
expect(sentA).toBe(false);
});
httpBackend.flush("/txn2", 1).done(function() {
httpBackend.flush("/txn1", 1).done(function() {
done();
});
});
});
it("should be able to send messages", function(done) {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "foo"
});
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
expect(res.event_id).toEqual("foo");
done();
});
httpBackend.flush("/txn1", 1);
});
});
});
+51
View File
@@ -0,0 +1,51 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
describe("MatrixClient retrying", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var userId = "@alice:localhost";
var accessToken = "aseukfgwef";
beforeEach(function() {
utils.beforeEach(this);
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken
});
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
});
xit("should retry according to MatrixScheduler.retryFn", function() {
});
xit("should queue according to MatrixScheduler.queueFn", function() {
});
xit("should mark events as EventStatus.NOT_SENT when giving up", function() {
});
xit("should mark events as EventStatus.QUEUED when queued", function() {
});
describe("resending", function() {
xit("should be able to resend a NOT_SENT event", function() {
});
xit("should be able to resend a sent event", function() {
});
});
});
@@ -0,0 +1,412 @@
"use strict";
var sdk = require("../..");
var EventStatus = sdk.EventStatus;
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
describe("MatrixClient room timelines", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var userId = "@alice:localhost";
var userName = "Alice";
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var otherUserId = "@bob:localhost";
var eventData;
var initialSync = {
end: "s_5_3",
presence: [],
rooms: [{
membership: "join",
room_id: roomId,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello"
})
]
},
state: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name"
}
}),
utils.mkMembership({
room: roomId, mship: "join", user: otherUserId, name: "Bob"
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}]
};
beforeEach(function(done) {
utils.beforeEach(this);
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken
});
eventData = {
chunk: [],
end: "end_",
start: "start_"
};
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, function() {
return eventData;
});
client.startClient();
httpBackend.flush("/pushrules").done(done);
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
});
describe("local echo events", function() {
it("should be added immediately after calling MatrixClient.sendEvent " +
"with EventStatus.SENDING and the right event.sender", function(done) {
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.sendTextMessage(roomId, "I am a fish", "txn1");
// check it was added
expect(room.timeline.length).toEqual(2);
// check status
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
// check member
var member = room.timeline[1].sender;
expect(member.userId).toEqual(userId);
expect(member.name).toEqual(userName);
httpBackend.flush("/events", 1).done(function() {
done();
});
});
httpBackend.flush("/initialSync", 1);
});
it("should be updated correctly when the send request finishes " +
"BEFORE the event comes down the event stream", function(done) {
var eventId = "$foo:bar";
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
});
eventData.chunk = [
utils.mkMessage({
body: "I am a fish", user: userId, room: roomId
})
];
eventData.chunk[0].event_id = eventId;
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
client.sendTextMessage(roomId, "I am a fish", "txn1").done(
function() {
expect(room.timeline[1].getId()).toEqual(eventId);
httpBackend.flush("/events", 1).done(function() {
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
});
httpBackend.flush("/txn1", 1);
});
httpBackend.flush("/initialSync", 1);
});
it("should be updated correctly when the send request finishes " +
"AFTER the event comes down the event stream", function(done) {
var eventId = "$foo:bar";
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
});
eventData.chunk = [
utils.mkMessage({
body: "I am a fish", user: userId, room: roomId
})
];
eventData.chunk[0].event_id = eventId;
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
var promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend.flush("/events", 1).done(function() {
// expect 3rd msg, it doesn't know this is the request is just did
expect(room.timeline.length).toEqual(3);
httpBackend.flush("/txn1", 1);
promise.done(function() {
expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
});
});
httpBackend.flush("/initialSync", 1);
});
});
describe("paginated events", function() {
var sbEvents;
var sbEndTok = "pagin_end";
beforeEach(function() {
sbEvents = [];
httpBackend.when("GET", "/messages").respond(200, function() {
return {
chunk: sbEvents,
start: "pagin_start",
end: sbEndTok
};
});
});
it("should set Room.oldState.paginationToken to null at the start" +
" of the timeline.", function(done) {
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
expect(room.timeline.length).toEqual(1);
expect(room.oldState.paginationToken).toBeNull();
done();
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1);
});
httpBackend.flush("/initialSync", 1);
});
it("should set the right event.sender values", function(done) {
// make an m.room.member event with prev_content
var oldMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: userName,
url: "mxc://some/url"
});
oldMshipEvent.prev_content = {
displayname: "Old Alice",
avatar_url: null,
membership: "join"
};
// set the list of events to return on scrollback
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I'm alice"
}),
oldMshipEvent,
utils.mkMessage({
user: userId, room: roomId, msg: "I'm old alice"
})
];
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
expect(room.timeline.length).toEqual(4);
var oldMsg = room.timeline[0];
expect(oldMsg.sender.name).toEqual("Old Alice");
var newMsg = room.timeline[2];
expect(newMsg.sender.name).toEqual(userName);
done();
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1);
});
httpBackend.flush("/initialSync", 1);
});
it("should add it them to the right place in the timeline", function(done) {
// set the list of events to return on scrollback
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I am new"
}),
utils.mkMessage({
user: userId, room: roomId, msg: "I am old"
})
];
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
expect(room.timeline.length).toEqual(3);
expect(room.timeline[0].event).toEqual(sbEvents[1]);
expect(room.timeline[1].event).toEqual(sbEvents[0]);
done();
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1);
});
httpBackend.flush("/initialSync", 1);
});
it("should use 'end' as the next pagination token", function(done) {
// set the list of events to return on scrollback
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I am new"
})
];
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
expect(room.oldState.paginationToken).toBeDefined();
client.scrollback(room, 1).done(function() {
expect(room.oldState.paginationToken).toEqual(sbEndTok);
done();
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1);
});
httpBackend.flush("/initialSync", 1);
});
});
describe("new events", function() {
it("should be added to the right place in the timeline", function(done) {
eventData.chunk = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMessage({user: userId, room: roomId})
];
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
var index = 0;
client.on("Room.timeline", function(event, rm, toStart) {
expect(toStart).toBe(false);
expect(rm).toEqual(room);
expect(event.event).toEqual(eventData.chunk[index]);
index += 1;
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1).done(function() {
expect(index).toEqual(2);
expect(room.timeline[room.timeline.length - 1].event).toEqual(
eventData.chunk[1]
);
expect(room.timeline[room.timeline.length - 2].event).toEqual(
eventData.chunk[0]
);
done();
});
});
httpBackend.flush("/initialSync", 1);
});
it("should set the right event.sender values", function(done) {
eventData.chunk = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMembership({
user: userId, room: roomId, mship: "join", name: "New Name"
}),
utils.mkMessage({user: userId, room: roomId})
];
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
httpBackend.flush("/events", 1).done(function() {
var preNameEvent = room.timeline[room.timeline.length - 3];
var postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender.name).toEqual(userName);
expect(postNameEvent.sender.name).toEqual("New Name");
done();
});
});
httpBackend.flush("/initialSync", 1);
});
it("should set the right room.name", function(done) {
eventData.chunk = [
utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 2"
}
})
];
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
var nameEmitCount = 0;
client.on("Room.name", function(rm) {
nameEmitCount += 1;
});
httpBackend.flush("/events", 1).done(function() {
expect(nameEmitCount).toEqual(1);
expect(room.name).toEqual("Room 2");
// do another round
eventData.chunk = [
utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 3"
}
})
];
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.flush("/events", 1).done(function() {
expect(nameEmitCount).toEqual(2);
expect(room.name).toEqual("Room 3");
done();
});
});
});
httpBackend.flush("/initialSync", 1);
});
it("should set the right room members", function(done) {
var userC = "@cee:bar";
var userD = "@dee:bar";
eventData.chunk = [
utils.mkMembership({
user: userC, room: roomId, mship: "join", name: "C"
}),
utils.mkMembership({
user: userC, room: roomId, mship: "invite", skey: userD
})
];
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
httpBackend.flush("/events", 1).done(function() {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC).name).toEqual("C");
expect(room.currentState.getMember(userC).membership).toEqual(
"join"
);
expect(room.currentState.getMember(userD).name).toEqual(userD);
expect(room.currentState.getMember(userD).membership).toEqual(
"invite"
);
done();
});
});
httpBackend.flush("/initialSync", 1);
});
});
});
+283
View File
@@ -0,0 +1,283 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
describe("MatrixClient syncing", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var selfUserId = "@alice:localhost";
var selfAccessToken = "aseukfgwef";
var otherUserId = "@bob:localhost";
beforeEach(function() {
utils.beforeEach(this);
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: selfUserId,
accessToken: selfAccessToken
});
httpBackend.when("GET", "/pushrules").respond(200, {});
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
});
describe("startClient", function() {
var initialSync = {
end: "s_5_3",
presence: [],
rooms: []
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: []
};
it("should start with /initialSync then move onto /events.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
done();
});
});
it("should pass the 'end' token from /initialSync to the from= param " +
" of /events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").check(function(req) {
expect(req.queryParams.from).toEqual(initialSync.end);
}).respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
done();
});
});
});
describe("users", function() {
var userA = "@alice:bar";
var userB = "@bob:bar";
var userC = "@claire:bar";
var initialSync = {
end: "s_5_3",
presence: [
utils.mkPresence({
user: userA, presence: "online"
}),
utils.mkPresence({
user: userB, presence: "unavailable"
})
],
rooms: []
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: [
// existing user change
utils.mkPresence({
user: userA, presence: "offline"
}),
// new user C
utils.mkPresence({
user: userC, presence: "online"
})
]
};
it("should create users for presence events from /initialSync and /events",
function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
expect(client.getUser(userA).presence).toEqual("offline");
expect(client.getUser(userB).presence).toEqual("unavailable");
expect(client.getUser(userC).presence).toEqual("online");
done();
});
});
});
describe("room state", function() {
var roomOne = "!foo:localhost";
var roomTwo = "!bar:localhost";
var msgText = "some text here";
var otherDisplayName = "Bob Smith";
var initialSync = {
end: "s_5_3",
presence: [],
rooms: [
{
membership: "join",
room_id: roomOne,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
})
]
},
state: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name"
}
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId
}
})
]
},
{
membership: "join",
room_id: roomTwo,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hiii"
})
]
},
state: [
utils.mkMembership({
room: roomTwo, mship: "join", user: otherUserId,
name: otherDisplayName
}),
utils.mkMembership({
room: roomTwo, mship: "join", user: selfUserId
}),
utils.mkEvent({
type: "m.room.create", room: roomTwo, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
]
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: selfUserId,
content: { name: "A new room name" }
}),
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: msgText
}),
utils.mkEvent({
type: "m.typing", room: roomTwo,
content: { user_ids: [otherUserId] }
})
]
};
it("should continually recalculate the right room name.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomOne);
// should have clobbered the name to the one from /events
expect(room.name).toEqual(eventData.chunk[0].content.name);
done();
});
});
it("should store the right events in the timeline.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
var 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) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
var 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) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomTwo);
var member = room.getMember(otherUserId);
expect(member).toBeDefined();
expect(member.typing).toEqual(true);
member = room.getMember(selfUserId);
expect(member).toBeDefined();
expect(member.typing).toEqual(false);
done();
});
});
xit("should update power levels for users in a room", function() {
});
xit("should update the room topic", function() {
});
});
describe("of a room", function() {
xit("should sync when a join event (which changes state) for the user" +
" arrives down the event stream (e.g. join from another device)", function() {
});
xit("should sync when the user explicitly calls joinRoom", function() {
});
});
});
+31 -12
View File
@@ -19,25 +19,37 @@ function HttpBackend() {
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() {
flush: function(path, numToFlush) {
var defer = q.defer();
var self = this;
console.log("HTTP backend flushing...");
var flushed = 0;
console.log(
"HTTP backend flushing... (path=%s numToFlush=%s)", path, numToFlush
);
var tryFlush = function() {
// if there's more real requests and more expected requests, flush 'em.
console.log(
" trying to flush queue => reqs=%s expected=%s",
self.requests.length, self.expectedRequests.length
" trying to flush queue => reqs=%s expected=%s [%s]",
self.requests.length, self.expectedRequests.length, path
);
if (self._takeFromQueue()) {
if (self._takeFromQueue(path)) {
// try again on the next tick.
console.log(" flushed. Trying for more.");
setTimeout(tryFlush, 0);
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 {
console.log(" no more flushes.");
console.log(" no more flushes. [%s]", path);
defer.resolve();
}
};
@@ -49,9 +61,10 @@ HttpBackend.prototype = {
/**
* 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() {
_takeFromQueue: function(path) {
var req = null;
var i, j;
var matchingReq, expectedReq, testResponse = null;
@@ -59,6 +72,7 @@ HttpBackend.prototype = {
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) ===
@@ -80,10 +94,14 @@ HttpBackend.prototype = {
}
testResponse = matchingReq.response;
console.log(" responding to %s", matchingReq.path);
var body = testResponse.body;
if (Object.prototype.toString.call(body) == "[object Function]") {
body = body(req.path, req.data);
}
req.callback(
testResponse.err, testResponse.response, testResponse.body
testResponse.err, testResponse.response, body
);
matchingReq = null;
}
}
if (testResponse) { // flushed something
@@ -150,7 +168,8 @@ Request.prototype = {
/**
* Respond with the given data when this request is satisfied.
* @param {Number} code The HTTP status code.
* @param {Object} data The HTTP JSON body.
* @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 = {
+118 -43
View File
@@ -1,4 +1,6 @@
"use strict";
var sdk = require("..");
var MatrixEvent = sdk.MatrixEvent;
/**
* Perform common actions before each test case, e.g. printing the test case
@@ -12,67 +14,140 @@ module.exports.beforeEach = function(testCase) {
};
/**
* Create a JSON object representing an Event.
* @param {string} type The event.type
* @param {string} room The event.room_id
* @param {string} userId The event.user_id
* @param {Object} content The event.content
* Create a spy for an object and automatically spy its methods.
* @param {*} constr The class constructor (used with 'new')
* @param {string} name The name of the class
* @return {Object} An instantiated object with spied methods/properties.
*/
module.exports.mock = function(constr, name) {
// By Tim Buschtöns
// http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
var HelperConstr = new Function(); // jshint ignore:line
HelperConstr.prototype = constr.prototype;
var result = new HelperConstr();
result.jasmineToString = function() {
return "mock" + (name ? " of " + name : "");
};
for (var key in constr.prototype) { // jshint ignore:line
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jasmine.createSpy((name || "mock") + '.' + key);
}
}
catch (ex) {
// Direct access to some non-function fields of DOM prototypes may
// cause exceptions.
// Overwriting will not work either in that case.
}
}
return result;
};
/**
* Create an Event.
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.user The event.user_id
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object} a JSON object representing this event.
*/
module.exports.mkEvent = function(type, room, userId, content) {
module.exports.mkEvent = function(opts) {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
var event = {
type: type,
room_id: room,
user_id: userId,
content: content,
type: opts.type,
room_id: opts.room,
user_id: opts.user,
content: opts.content,
event_id: "$" + Math.random() + "-" + Math.random()
};
if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
if (opts.skey) {
event.state_key = opts.skey;
}
else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
"m.room.power_levels", "m.room.topic",
"com.example.state"].indexOf(type) !== -1) {
"com.example.state"].indexOf(opts.type) !== -1) {
event.state_key = "";
}
return event;
return opts.event ? new MatrixEvent(event) : event;
};
/**
* Create an m.room.member POJO.
* @param {string} room The room ID for the event.
* @param {string} membership The content.membership for the event.
* @param {string} userId The user ID for the event.
* @param {string} otherUserId The other user ID for the event if applicable
* Create an m.presence event.
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
*/
module.exports.mkPresence = function(opts) {
if (!opts.user) {
throw new Error("Missing user");
}
var event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
content: {
user_id: opts.user,
avatar_url: opts.url,
displayname: opts.name,
last_active_ago: opts.ago,
presence: opts.presence || "offline"
}
};
return opts.event ? new MatrixEvent(event) : event;
};
/**
* Create an m.room.member event.
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.skey The other user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} displayName The content.displayname for the event.
* @param {string} avatarUrl The content.avatar_url for the event.
* @return {Object} The event
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
module.exports.mkMembership = function(room, membership, userId, otherUserId,
displayName, avatarUrl) {
var event = module.exports.mkEvent("m.room.member", room, userId, {
membership: membership,
displayname: displayName,
avatar_url: avatarUrl
});
event.state_key = userId;
if (["invite", "ban"].indexOf(membership) !== -1) {
event.state_key = otherUserId;
module.exports.mkMembership = function(opts) {
opts.type = "m.room.member";
if (!opts.skey) {
opts.skey = opts.user;
}
return event;
if (!opts.mship) {
throw new Error("Missing .mship => " + JSON.stringify(opts));
}
opts.content = {
membership: opts.mship
};
if (opts.name) { opts.content.displayname = opts.name; }
if (opts.url) { opts.content.avatar_url = opts.url; }
return module.exports.mkEvent(opts);
};
/**
* Create an m.room.message POJO.
* @param {string} room The room ID for the event.
* @param {string} userId The user ID for the event.
* @param {string} msg The content.body for the event.
* @return {Object} The event
* Create an m.room.message event.
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.msg Optional. The content.body for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
module.exports.mkMessage = function(room, userId, msg) {
if (!msg) {
msg = "Random->" + Math.random();
module.exports.mkMessage = function(opts) {
opts.type = "m.room.message";
if (!opts.msg) {
opts.msg = "Random->" + Math.random();
}
return module.exports.mkEvent("m.room.message", room, userId, {
if (!opts.room || !opts.user) {
throw new Error("Missing .room or .user from %s", opts);
}
opts.content = {
msgtype: "m.text",
body: msg
});
body: opts.msg
};
return module.exports.mkEvent(opts);
};
+305
View File
@@ -0,0 +1,305 @@
"use strict";
var PushProcessor = require("../../lib/pushprocessor");
var MatrixEvent = MatrixEvent;
var utils = require("../test-utils");
describe('NotificationService', function() {
var testUserId = "@ali:matrix.org";
var testDisplayName = "Alice M";
var testRoomId = "!fl1bb13:localhost";
var testEvent;
var pushProcessor;
// These would be better if individual rules were configured in the tests themselves.
var matrixClient = {
getRoom: function() {
return {
currentState: {
getMember: function() {
return {
name: testDisplayName
};
},
members: {}
}
};
},
credentials: {
userId: testUserId
},
pushRules: {
"device": {},
"global": {
"content": [
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight"
}
],
"enabled": true,
"pattern": "ali",
"rule_id": ".m.rule.contains_user_name"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight"
}
],
"enabled": true,
"pattern": "coffee",
"rule_id": "coffee"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight"
}
],
"enabled": true,
"pattern": "foo*bar",
"rule_id": "foobar"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight"
}
],
"enabled": true,
"pattern": "p[io]ng",
"rule_id": "pingpong"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight"
}
],
"enabled": true,
"pattern": "I ate [0-9] pies",
"rule_id": "pies"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight"
}
],
"enabled": true,
"pattern": "b[!ai]ke",
"rule_id": "bakebike"
}
],
"override": [
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight"
}
],
"conditions": [
{
"kind": "contains_display_name"
}
],
"enabled": true,
"rule_id": ".m.rule.contains_display_name"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
}
],
"conditions": [
{
"is": "2",
"kind": "room_member_count"
}
],
"enabled": true,
"rule_id": ".m.rule.room_one_to_one"
}
],
"room": [],
"sender": [],
"underride": [
{
"actions": [
"dont-notify"
],
"conditions": [
{
"key": "content.msgtype",
"kind": "event_match",
"pattern": "m.notice"
}
],
"enabled": true,
"rule_id": ".m.rule.suppress_notices"
},
{
"actions": [
"notify",
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [],
"enabled": true,
"rule_id": ".m.rule.fallback"
}
]
}
}
};
beforeEach(function() {
testEvent = utils.mkEvent({
type: "m.room.message",
room: testRoomId,
user: "@alfred:localhost",
event: true,
content: {
body: "",
msgtype: "m.text"
}
});
pushProcessor = new PushProcessor(matrixClient);
console.log("badger: " + JSON.stringify(pushProcessor));
});
// User IDs
it('should bing on a user ID.', function() {
testEvent.event.content.body = "Hello @ali:matrix.org, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a partial user ID with an @.', function() {
testEvent.event.content.body = "Hello @ali, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a partial user ID without @.', function() {
testEvent.event.content.body = "Hello ali, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a case-insensitive user ID.', function() {
testEvent.event.content.body = "Hello @AlI:matrix.org, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(true);
});
// Display names
it('should bing on a display name.', function() {
testEvent.event.content.body = "Hello Alice M, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a case-insensitive display name.', function() {
testEvent.event.content.body = "Hello ALICE M, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(true);
});
// Bing words
it('should bing on a bing word.', function() {
testEvent.event.content.body = "I really like coffee";
var actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on case-insensitive bing words.', function() {
testEvent.event.content.body = "Coffee is great";
var actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on wildcard (.*) bing words.', function() {
testEvent.event.content.body = "It was foomahbar I think.";
var actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on character group ([abc]) bing words.', function() {
testEvent.event.content.body = "Ping!";
var actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content.body = "Pong!";
actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on character range ([a-z]) bing words.', function() {
testEvent.event.content.body = "I ate 6 pies";
var actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on character negation ([!a]) bing words.', function() {
testEvent.event.content.body = "boke";
var actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content.body = "bake";
actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(false);
});
// invalid
it('should gracefully handle bad input.', function() {
testEvent.event.content.body = { "foo": "bar" };
var actions = pushProcessor.actionsForEvent(testEvent.event);
expect(actions.tweaks.highlight).toEqual(false);
});
});
+185 -38
View File
@@ -1,7 +1,6 @@
"use strict";
var sdk = require("../..");
var RoomMember = sdk.RoomMember;
var MatrixEvent = sdk.MatrixEvent;
var utils = require("../test-utils");
describe("RoomMember", function() {
@@ -16,48 +15,196 @@ describe("RoomMember", function() {
member = new RoomMember(roomId, userA);
});
it("setPowerLevelEvent should set 'powerLevel' and 'powerLevelNorm'.",
function() {
var event = new MatrixEvent(
utils.mkEvent("m.room.power_levels", roomId, userA, {
users_default: 20,
users: {
"@bertha:bar": 200,
"@invalid:user": 10 // shouldn't barf on this.
}
})
);
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(20);
expect(member.powerLevelNorm).toEqual(10);
describe("setPowerLevelEvent", function() {
it("should set 'powerLevel' and 'powerLevelNorm'.", function() {
var event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@bertha:bar": 200,
"@invalid:user": 10 // shouldn't barf on this.
}
},
event: true
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(20);
expect(member.powerLevelNorm).toEqual(10);
var memberB = new RoomMember(roomId, userB);
memberB.setPowerLevelEvent(event);
expect(memberB.powerLevel).toEqual(200);
expect(memberB.powerLevelNorm).toEqual(100);
var memberB = new RoomMember(roomId, userB);
memberB.setPowerLevelEvent(event);
expect(memberB.powerLevel).toEqual(200);
expect(memberB.powerLevelNorm).toEqual(100);
});
it("should emit 'RoomMember.powerLevel' if the power level changes.",
function() {
var event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@bertha:bar": 200,
"@invalid:user": 10 // shouldn't barf on this.
}
},
event: true
});
var emitCount = 0;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember).toEqual(member);
expect(emitEvent).toEqual(event);
});
member.setPowerLevelEvent(event);
expect(emitCount).toEqual(1);
member.setPowerLevelEvent(event); // no-op
expect(emitCount).toEqual(1);
});
});
it("setTypingEvent should set 'typing'", function() {
member.typing = false;
var memberB = new RoomMember(roomId, userB);
memberB.typing = true;
var memberC = new RoomMember(roomId, userC);
memberC.typing = true;
describe("setTypingEvent", function() {
it("should set 'typing'", function() {
member.typing = false;
var memberB = new RoomMember(roomId, userB);
memberB.typing = true;
var memberC = new RoomMember(roomId, userC);
memberC.typing = true;
var event = utils.mkEvent({
type: "m.typing",
user: userA,
room: roomId,
content: {
user_ids: [
userA, userC
]
},
event: true
});
member.setTypingEvent(event);
memberB.setTypingEvent(event);
memberC.setTypingEvent(event);
expect(member.typing).toEqual(true);
expect(memberB.typing).toEqual(false);
expect(memberC.typing).toEqual(true);
});
it("should emit 'RoomMember.typing' if the typing state changes",
function() {
var event = utils.mkEvent({
type: "m.typing",
room: roomId,
content: {
user_ids: [
userA, userC
]
},
event: true
});
var emitCount = 0;
member.on("RoomMember.typing", function(ev, mem) {
expect(mem).toEqual(member);
expect(ev).toEqual(event);
emitCount += 1;
});
member.typing = false;
member.setTypingEvent(event);
expect(emitCount).toEqual(1);
member.setTypingEvent(event); // no-op
expect(emitCount).toEqual(1);
});
});
describe("setMembershipEvent", function() {
var joinEvent = utils.mkMembership({
event: true,
mship: "join",
user: userA,
room: roomId,
name: "Alice"
});
var inviteEvent = utils.mkMembership({
event: true,
mship: "invite",
user: userB,
skey: userA,
room: roomId
});
it("should set 'membership' and assign the event to 'events.member'.",
function() {
member.setMembershipEvent(inviteEvent);
expect(member.membership).toEqual("invite");
expect(member.events.member).toEqual(inviteEvent);
member.setMembershipEvent(joinEvent);
expect(member.membership).toEqual("join");
expect(member.events.member).toEqual(joinEvent);
});
it("should set 'name' based on user_id, displayname and room state",
function() {
var roomState = {
getStateEvents: function(type) {
if (type !== "m.room.member") { return []; }
return [
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userB
}),
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userC, name: "Alice"
}),
joinEvent
];
}
};
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent);
expect(member.name).toEqual("Alice"); // prefer displayname
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alice"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).not.toEqual(-1);
});
it("should emit 'RoomMember.membership' if the membership changes", function() {
var emitCount = 0;
member.on("RoomMember.membership", function(ev, mem) {
emitCount += 1;
expect(mem).toEqual(member);
expect(ev).toEqual(inviteEvent);
});
member.setMembershipEvent(inviteEvent);
expect(emitCount).toEqual(1);
member.setMembershipEvent(inviteEvent); // no-op
expect(emitCount).toEqual(1);
});
it("should emit 'RoomMember.name' if the name changes", function() {
var emitCount = 0;
member.on("RoomMember.name", function(ev, mem) {
emitCount += 1;
expect(mem).toEqual(member);
expect(ev).toEqual(joinEvent);
});
member.setMembershipEvent(joinEvent);
expect(emitCount).toEqual(1);
member.setMembershipEvent(joinEvent); // no-op
expect(emitCount).toEqual(1);
});
var event = new MatrixEvent(
utils.mkEvent("m.typing", roomId, userA, {
user_ids: [
userA, userC
]
})
);
member.setTypingEvent(event);
memberB.setTypingEvent(event);
memberC.setTypingEvent(event);
expect(member.typing).toEqual(true);
expect(memberB.typing).toEqual(false);
expect(memberC.typing).toEqual(true);
});
});
+282
View File
@@ -0,0 +1,282 @@
"use strict";
var sdk = require("../..");
var RoomState = sdk.RoomState;
var RoomMember = sdk.RoomMember;
var utils = require("../test-utils");
describe("RoomState", function() {
var roomId = "!foo:bar";
var userA = "@alice:bar";
var userB = "@bob:bar";
var state;
beforeEach(function() {
utils.beforeEach(this);
state = new RoomState(roomId);
state.setStateEvents([
utils.mkMembership({ // userA joined
event: true, mship: "join", user: userA, room: roomId
}),
utils.mkMembership({ // userB joined
event: true, mship: "join", user: userB, room: roomId
}),
utils.mkEvent({ // Room name is "Room name goes here"
type: "m.room.name", user: userA, room: roomId, event: true, content: {
name: "Room name goes here"
}
}),
utils.mkEvent({ // Room creation
type: "m.room.create", user: userA, room: roomId, event: true, content: {
creator: userA
}
})
]);
});
describe("getMembers", function() {
it("should return an empty list if there are no members", function() {
state = new RoomState(roomId);
expect(state.getMembers().length).toEqual(0);
});
it("should return a member for each m.room.member event", function() {
var 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);
});
});
describe("getMember", function() {
it("should return null if there is no member", function() {
expect(state.getMember("@no-one:here")).toEqual(null);
});
it("should return a member if they exist", function() {
expect(state.getMember(userB)).toBeDefined();
});
it("should return a member which changes as state changes", function() {
var member = state.getMember(userB);
expect(member.membership).toEqual("join");
expect(member.name).toEqual(userB);
state.setStateEvents([
utils.mkMembership({
room: roomId, user: userB, mship: "leave", event: true,
name: "BobGone"
})
]);
expect(member.membership).toEqual("leave");
expect(member.name).toEqual("BobGone");
});
});
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 which doesn't change when the state is updated",
function() {
var preLeaveUser = state.getSentinelMember(userA);
state.setStateEvents([
utils.mkMembership({
room: roomId, user: userA, mship: "leave", event: true,
name: "AliceIsGone"
})
]);
var postLeaveUser = state.getSentinelMember(userA);
expect(preLeaveUser.membership).toEqual("join");
expect(preLeaveUser.name).toEqual(userA);
expect(postLeaveUser.membership).toEqual("leave");
expect(postLeaveUser.name).toEqual("AliceIsGone");
});
});
describe("getStateEvents", function() {
it("should return null if a state_key was specified and there was no match",
function() {
expect(state.getStateEvents("foo.bar.baz", "keyname")).toEqual(null);
});
it("should return an empty list if a state_key was not specified and there" +
" was no match", function() {
expect(state.getStateEvents("foo.bar.baz")).toEqual([]);
});
it("should return a list of matching events if no state_key was specified",
function() {
var 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);
});
it("should return a single MatrixEvent if a state_key was specified",
function() {
var event = state.getStateEvents("m.room.member", userA);
expect(event.getContent()).toEqual({
membership: "join"
});
});
});
describe("setStateEvents", function() {
it("should emit 'RoomState.members' for each m.room.member event", function() {
var memberEvents = [
utils.mkMembership({
user: "@cleo:bar", mship: "invite", room: roomId, event: true
}),
utils.mkMembership({
user: "@daisy:bar", mship: "join", room: roomId, event: true
})
];
var emitCount = 0;
state.on("RoomState.members", function(ev, st, mem) {
expect(ev).toEqual(memberEvents[emitCount]);
expect(st).toEqual(state);
expect(mem).toEqual(state.getMember(ev.getSender()));
emitCount += 1;
});
state.setStateEvents(memberEvents);
expect(emitCount).toEqual(2);
});
it("should emit 'RoomState.newMember' for each new member added", function() {
var memberEvents = [
utils.mkMembership({
user: "@cleo:bar", mship: "invite", room: roomId, event: true
}),
utils.mkMembership({
user: "@daisy:bar", mship: "join", room: roomId, event: true
})
];
var emitCount = 0;
state.on("RoomState.newMember", function(ev, st, mem) {
expect(mem.userId).toEqual(memberEvents[emitCount].getSender());
expect(mem.membership).toBeFalsy(); // not defined yet
emitCount += 1;
});
state.setStateEvents(memberEvents);
expect(emitCount).toEqual(2);
});
it("should emit 'RoomState.events' for each state event", function() {
var events = [
utils.mkMembership({
user: "@cleo:bar", mship: "invite", room: roomId, event: true
}),
utils.mkEvent({
user: userB, room: roomId, type: "m.room.topic", event: true,
content: {
topic: "boo!"
}
}),
utils.mkMessage({ // Not a state event
user: userA, room: roomId, event: true
})
];
var emitCount = 0;
state.on("RoomState.events", function(ev, st) {
expect(ev).toEqual(events[emitCount]);
expect(st).toEqual(state);
emitCount += 1;
});
state.setStateEvents(events);
expect(emitCount).toEqual(2);
});
it("should call setPowerLevelEvent on each RoomMember for m.room.power_levels",
function() {
// mock up the room members
state.members[userA] = utils.mock(RoomMember);
state.members[userB] = utils.mock(RoomMember);
var powerLevelEvent = utils.mkEvent({
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 50,
events_default: 25
}
});
state.setStateEvents([powerLevelEvent]);
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(
powerLevelEvent
);
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(
powerLevelEvent
);
});
it("should call setPowerLevelEvent on a new RoomMember if power levels exist",
function() {
var userC = "@cleo:bar";
var memberEvent = utils.mkMembership({
mship: "join", user: userC, room: roomId, event: true
});
var powerLevelEvent = utils.mkEvent({
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 50,
events_default: 25,
users: {}
}
});
state.setStateEvents([powerLevelEvent]);
state.setStateEvents([memberEvent]);
// 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].powerLevel).toEqual(10);
});
it("should call setMembershipEvent on the right RoomMember", function() {
// mock up the room members
state.members[userA] = utils.mock(RoomMember);
state.members[userB] = utils.mock(RoomMember);
var memberEvent = utils.mkMembership({
user: userB, mship: "leave", room: roomId, event: true
});
state.setStateEvents([memberEvent]);
expect(state.members[userA].setMembershipEvent).not.toHaveBeenCalled();
expect(state.members[userB].setMembershipEvent).toHaveBeenCalledWith(
memberEvent, state
);
});
});
describe("setTypingEvent", function() {
it("should call setTypingEvent on each RoomMember", function() {
var typingEvent = utils.mkEvent({
type: "m.typing", room: roomId, event: true, content: {
user_ids: [userA]
}
});
// mock up the room members
state.members[userA] = utils.mock(RoomMember);
state.members[userB] = utils.mock(RoomMember);
state.setTypingEvent(typingEvent);
expect(state.members[userA].setTypingEvent).toHaveBeenCalledWith(
typingEvent
);
expect(state.members[userB].setTypingEvent).toHaveBeenCalledWith(
typingEvent
);
});
});
});
+322 -46
View File
@@ -1,7 +1,7 @@
"use strict";
var sdk = require("../..");
var Room = sdk.Room;
var MatrixEvent = sdk.MatrixEvent;
var RoomState = sdk.RoomState;
var utils = require("../test-utils");
describe("Room", function() {
@@ -15,10 +15,14 @@ describe("Room", function() {
beforeEach(function() {
utils.beforeEach(this);
room = new Room(roomId);
// mock RoomStates
room.oldState = utils.mock(sdk.RoomState, "oldState");
room.currentState = utils.mock(sdk.RoomState, "currentState");
});
describe("getMember", function() {
beforeEach(function() {
// clobber members property with test data
room.currentState.members = {
"@alice:bar": {
userId: userA,
@@ -36,13 +40,68 @@ describe("Room", function() {
});
});
describe("addEvents", function() {
var events = [
utils.mkMessage({
room: roomId, user: userA, msg: "changing room name", event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
})
];
it("should call RoomState.setTypingEvent on m.typing events", function() {
room.currentState = utils.mock(RoomState);
var typing = utils.mkEvent({
room: roomId, type: "m.typing", event: true, content: {
user_ids: [userA]
}
});
room.addEvents([typing]);
expect(room.currentState.setTypingEvent).toHaveBeenCalledWith(typing);
});
it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", function() {
expect(function() { room.addEvents(events, "foo"); }).toThrow();
});
it("should replace a timeline event if dupe strategy is 'replace'", function() {
// make a duplicate
var dupe = utils.mkMessage({
room: roomId, user: userA, msg: "dupe", event: true
});
dupe.event.event_id = events[0].getId();
room.addEvents(events);
expect(room.timeline[0]).toEqual(events[0]);
room.addEvents([dupe], "replace");
expect(room.timeline[0]).toEqual(dupe);
});
it("should ignore a given dupe event if dupe strategy is 'ignore'", function() {
// make a duplicate
var dupe = utils.mkMessage({
room: roomId, user: userA, msg: "dupe", event: true
});
dupe.event.event_id = events[0].getId();
room.addEvents(events);
expect(room.timeline[0]).toEqual(events[0]);
room.addEvents([dupe], "ignore");
expect(room.timeline[0]).toEqual(events[0]);
});
});
describe("addEventsToTimeline", function() {
var events = [
new MatrixEvent(utils.mkMessage(roomId, userA, "changing room name")),
new MatrixEvent(utils.mkEvent("m.room.name", roomId, userA, {
name: "New Room Name"
}))
utils.mkMessage({
room: roomId, user: userA, msg: "changing room name", event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
})
];
it("should be able to add events to the end", function() {
room.addEventsToTimeline(events);
expect(room.timeline.length).toEqual(2);
@@ -56,14 +115,265 @@ describe("Room", function() {
expect(room.timeline[0]).toEqual(events[1]);
expect(room.timeline[1]).toEqual(events[0]);
});
it("should emit 'Room.timeline' events when added to the end",
function() {
var callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
callCount += 1;
expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]);
expect(emitRoom).toEqual(room);
expect(toStart).toBeFalsy();
});
room.addEventsToTimeline(events);
expect(callCount).toEqual(2);
});
it("should emit 'Room.timeline' events when added to the start",
function() {
var callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
callCount += 1;
expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]);
expect(emitRoom).toEqual(room);
expect(toStart).toBe(true);
});
room.addEventsToTimeline(events, true);
expect(callCount).toEqual(2);
});
it("should set event.sender for new and old events", function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
var oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
});
var oldEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Old Room Name" }
});
room.addEventsToTimeline([newEv]);
expect(newEv.sender).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true);
expect(oldEv.sender).toEqual(oldSentinel);
});
it("should set event.target for new and old m.room.member events",
function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
var oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
});
var oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true
});
room.addEventsToTimeline([newEv]);
expect(newEv.target).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true);
expect(oldEv.target).toEqual(oldSentinel);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value", function() {
room.oldState = utils.mock(RoomState);
room.currentState = utils.mock(RoomState);
var events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
];
room.addEventsToTimeline(events);
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
[events[0]]
);
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
[events[1]]
);
expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(true);
expect(room.oldState.setStateEvents).not.toHaveBeenCalled();
// test old
room.addEventsToTimeline(events, true);
expect(room.oldState.setStateEvents).toHaveBeenCalledWith(
[events[0]]
);
expect(room.oldState.setStateEvents).toHaveBeenCalledWith(
[events[1]]
);
expect(events[0].forwardLooking).toBe(false);
expect(events[1].forwardLooking).toBe(false);
});
});
describe("calculate (Room Name)", function() {
describe("getJoinedMembers", function() {
it("should return members whose membership is 'join'", function() {
room.currentState.getMembers.andCallFake(function() {
return [
{ userId: "@alice:bar", membership: "join" },
{ userId: "@bob:bar", membership: "invite" },
{ userId: "@cleo:bar", membership: "leave" }
];
});
var res = room.getJoinedMembers();
expect(res.length).toEqual(1);
expect(res[0].userId).toEqual("@alice:bar");
});
it("should return an empty list if no membership is 'join'", function() {
room.currentState.getMembers.andCallFake(function() {
return [
{ userId: "@bob:bar", membership: "invite" }
];
});
var res = room.getJoinedMembers();
expect(res.length).toEqual(0);
});
});
describe("hasMembershipState", function() {
it("should return true for a matching userId and membership",
function() {
room.currentState.getMembers.andCallFake(function() {
return [
{ userId: "@alice:bar", membership: "join" },
{ userId: "@bob:bar", membership: "invite" }
];
});
expect(room.hasMembershipState("@bob:bar", "invite")).toBe(true);
});
it("should return false if match membership but no match userId",
function() {
room.currentState.getMembers.andCallFake(function() {
return [
{ userId: "@alice:bar", membership: "join" }
];
});
expect(room.hasMembershipState("@bob:bar", "join")).toBe(false);
});
it("should return false if match userId but no match membership",
function() {
room.currentState.getMembers.andCallFake(function() {
return [
{ userId: "@alice:bar", membership: "join" }
];
});
expect(room.hasMembershipState("@alice:bar", "ban")).toBe(false);
});
it("should return false if no match membership or userId",
function() {
room.currentState.getMembers.andCallFake(function() {
return [
{ userId: "@alice:bar", membership: "join" }
];
});
expect(room.hasMembershipState("@bob:bar", "invite")).toBe(false);
});
it("should return false if no members exist",
function() {
room.currentState.getMembers.andCallFake(function() {
return [];
});
expect(room.hasMembershipState("@foo:bar", "join")).toBe(false);
});
});
describe("recalculate (Room Name)", function() {
var stateLookup = {
// event.type + "$" event.state_key : MatrixEvent
};
var mockRoomState = {
getStateEvents: function(type, key) {
var setJoinRule = function(rule) {
stateLookup["m.room.join_rules$"] = utils.mkEvent({
type: "m.room.join_rules", room: roomId, user: userA, content: {
join_rule: rule
}, event: true
});
};
var setAliases = function(aliases, stateKey) {
if (!stateKey) { stateKey = "flibble"; }
stateLookup["m.room.aliases$" + stateKey] = utils.mkEvent({
type: "m.room.aliases", room: roomId, skey: stateKey, content: {
aliases: aliases
}, event: true
});
};
var setRoomName = function(name) {
stateLookup["m.room.name$"] = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, content: {
name: name
}, event: true
});
};
var addMember = function(userId, state) {
if (!state) { state = "join"; }
stateLookup["m.room.member$" + userId] = utils.mkMembership({
room: roomId, mship: state, user: userId, skey: userId, event: true
});
};
beforeEach(function() {
stateLookup = {};
room.currentState.getStateEvents.andCallFake(function(type, key) {
if (key === undefined) {
var prefix = type + "$";
var list = [];
@@ -78,9 +388,9 @@ describe("Room", function() {
else {
return stateLookup[type + "$" + key];
}
},
getMembers: function() {
var memberEvents = this.getStateEvents("m.room.member");
});
room.currentState.getMembers.andCallFake(function() {
var memberEvents = room.currentState.getStateEvents("m.room.member");
var members = [];
for (var i = 0; i < memberEvents.length; i++) {
members.push({
@@ -91,41 +401,7 @@ describe("Room", function() {
});
}
return members;
}
};
var setJoinRule = function(rule) {
stateLookup["m.room.join_rules$"] = new MatrixEvent(
utils.mkEvent("m.room.join_rules", roomId, userA, {
join_rule: rule
})
);
};
var setAliases = function(aliases, stateKey) {
if (!stateKey) { stateKey = "flibble"; }
stateLookup["m.room.aliases$" + stateKey] = new MatrixEvent(
utils.mkEvent("m.room.aliases", roomId, stateKey, {
aliases: aliases
})
);
};
var setRoomName = function(name) {
stateLookup["m.room.name$"] = new MatrixEvent(
utils.mkEvent("m.room.name", roomId, userA, {
name: name
})
);
};
var addMember = function(userId, state) {
if (!state) { state = "join"; }
stateLookup["m.room.member$" + userId] = new MatrixEvent(
utils.mkMembership(roomId, state, userId, userId)
);
};
beforeEach(function() {
stateLookup = {};
room.currentState = mockRoomState;
});
});
it("should return the names of members in a private (invite join_rules)" +
+349
View File
@@ -0,0 +1,349 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var MatrixScheduler = sdk.MatrixScheduler;
var MatrixError = sdk.MatrixError;
var utils = require("../test-utils");
describe("MatrixScheduler", function() {
var scheduler;
var retryFn, queueFn;
var defer;
var roomId = "!foo:bar";
var eventA = utils.mkMessage({
user: "@alice:bar", room: roomId, event: true
});
var eventB = utils.mkMessage({
user: "@alice:bar", room: roomId, event: true
});
beforeEach(function() {
utils.beforeEach(this);
jasmine.Clock.useMock();
scheduler = new MatrixScheduler(function(ev, attempts, err) {
if (retryFn) {
return retryFn(ev, attempts, err);
}
return -1;
}, function(event) {
if (queueFn) {
return queueFn(event);
}
return null;
});
retryFn = null;
queueFn = null;
defer = q.defer();
});
it("should process events in a queue in a FIFO manner", function(done) {
retryFn = function() {
return 0;
};
queueFn = function() {
return "one_big_queue";
};
var deferA = q.defer();
var deferB = q.defer();
var resolvedA = false;
scheduler.setProcessFunction(function(event) {
if (resolvedA) {
expect(event).toEqual(eventB);
return deferB.promise;
}
else {
expect(event).toEqual(eventA);
return deferA.promise;
}
});
scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB).done(function() {
expect(resolvedA).toBe(true);
done();
});
deferA.resolve({});
resolvedA = true;
deferB.resolve({});
});
it("should invoke the retryFn on failure and wait the amount of time specified",
function(done) {
var waitTimeMs = 1500;
var retryDefer = q.defer();
retryFn = function() {
retryDefer.resolve();
return waitTimeMs;
};
queueFn = function() { return "yep"; };
var procCount = 0;
scheduler.setProcessFunction(function(ev) {
procCount += 1;
if (procCount === 1) {
expect(ev).toEqual(eventA);
return defer.promise;
}
else if (procCount === 2) {
// don't care about this defer
return q.defer().promise;
}
expect(procCount).toBeLessThan(3);
});
scheduler.queueEvent(eventA);
expect(procCount).toEqual(1);
defer.reject({});
retryDefer.promise.done(function() {
expect(procCount).toEqual(1);
jasmine.Clock.tick(waitTimeMs);
expect(procCount).toEqual(2);
done();
});
});
it("should give up if the retryFn on failure returns -1 and try the next event",
function(done) {
// Queue A & B.
// Reject A and return -1 on retry.
// Expect B to be tried next and the promise for A to be rejected.
retryFn = function() {
return -1;
};
queueFn = function() { return "yep"; };
var deferA = q.defer();
var deferB = q.defer();
var procCount = 0;
scheduler.setProcessFunction(function(ev) {
procCount += 1;
if (procCount === 1) {
expect(ev).toEqual(eventA);
return deferA.promise;
}
else if (procCount === 2) {
expect(ev).toEqual(eventB);
return deferB.promise;
}
expect(procCount).toBeLessThan(3);
});
var globalA = scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
expect(procCount).toEqual(1);
deferA.reject({});
globalA.catch(function() {
expect(procCount).toEqual(2);
done();
});
});
it("should treat each queue separately", function(done) {
// Queue messages A B C D.
// Bucket A&D into queue_A
// Bucket B&C into queue_B
// Expect to have processFn invoked for A&B.
// Resolve A.
// Expect to have processFn invoked for D.
var eventC = utils.mkMessage({user: "@a:bar", room: roomId, event: true});
var eventD = utils.mkMessage({user: "@b:bar", room: roomId, event: true});
var buckets = {};
buckets[eventA.getId()] = "queue_A";
buckets[eventD.getId()] = "queue_A";
buckets[eventB.getId()] = "queue_B";
buckets[eventC.getId()] = "queue_B";
retryFn = function() {
return 0;
};
queueFn = function(event) {
return buckets[event.getId()];
};
var expectOrder = [
eventA.getId(), eventB.getId(), eventD.getId()
];
var deferA = q.defer();
scheduler.setProcessFunction(function(event) {
var id = expectOrder.shift();
expect(id).toEqual(event.getId());
if (expectOrder.length === 0) {
done();
}
return id === eventA.getId() ? deferA.promise : defer.promise;
});
scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
scheduler.queueEvent(eventC);
scheduler.queueEvent(eventD);
// wait a bit then resolve A and we should get D (not C) next.
setTimeout(function() {
deferA.resolve({});
}, 1000);
jasmine.Clock.tick(1000);
});
describe("queueEvent", function() {
it("should return null if the event shouldn't be queued", function() {
queueFn = function() {
return null;
};
expect(scheduler.queueEvent(eventA)).toEqual(null);
});
it("should return a Promise if the event is queued", function() {
queueFn = function() {
return "yep";
};
var prom = scheduler.queueEvent(eventA);
expect(prom).toBeDefined();
expect(prom.then).toBeDefined();
});
});
describe("getQueueForEvent", function() {
it("should return null if the event doesn't map to a queue name", function() {
queueFn = function() {
return null;
};
expect(scheduler.getQueueForEvent(eventA)).toBeNull();
});
it("should return null if the mapped queue doesn't exist", function() {
queueFn = function() {
return "yep";
};
expect(scheduler.getQueueForEvent(eventA)).toBeNull();
});
it("should return a list of events in the queue and modifications to" +
" the list should not affect the underlying queue.", function() {
queueFn = function() {
return "yep";
};
scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
var queue = scheduler.getQueueForEvent(eventA);
expect(queue.length).toEqual(2);
expect(queue).toEqual([eventA, eventB]);
// modify the queue
var eventC = utils.mkMessage(
{user: "@a:bar", room: roomId, event: true}
);
queue.push(eventC);
var queueAgain = scheduler.getQueueForEvent(eventA);
expect(queueAgain.length).toEqual(2);
});
it("should return a list of events in the queue and modifications to" +
" an event in the queue should affect the underlying queue.", function() {
queueFn = function() {
return "yep";
};
scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
var queue = scheduler.getQueueForEvent(eventA);
queue[1].event.content.body = "foo";
var queueAgain = scheduler.getQueueForEvent(eventA);
expect(queueAgain[1].event.content.body).toEqual("foo");
});
});
describe("removeEventFromQueue", function() {
it("should return false if the event doesn't map to a queue name", function() {
queueFn = function() {
return null;
};
expect(scheduler.removeEventFromQueue(eventA)).toBe(false);
});
it("should return false if the event isn't in the queue", function() {
queueFn = function() {
return "yep";
};
expect(scheduler.removeEventFromQueue(eventA)).toBe(false);
});
it("should return true if the event was removed", function() {
queueFn = function() {
return "yep";
};
scheduler.queueEvent(eventA);
expect(scheduler.removeEventFromQueue(eventA)).toBe(true);
});
});
describe("setProcessFunction", function() {
it("should call the processFn if there are queued events", function() {
queueFn = function() {
return "yep";
};
var procCount = 0;
scheduler.queueEvent(eventA);
scheduler.setProcessFunction(function(ev) {
procCount += 1;
expect(ev).toEqual(eventA);
return defer.promise;
});
expect(procCount).toEqual(1);
});
it("should not call the processFn if there are no queued events", function() {
queueFn = function() {
return "yep";
};
var procCount = 0;
scheduler.setProcessFunction(function(ev) {
procCount += 1;
return defer.promise;
});
expect(procCount).toEqual(0);
});
});
describe("QUEUE_MESSAGES", function() {
it("should queue m.room.message events only", function() {
expect(MatrixScheduler.QUEUE_MESSAGES(eventA)).toEqual("message");
expect(MatrixScheduler.QUEUE_MESSAGES(
utils.mkMembership({
user: "@alice:bar", room: roomId, mship: "join", event: true
})
)).toEqual(null);
});
});
describe("RETRY_BACKOFF_RATELIMIT", function() {
it("should wait at least the time given on M_LIMIT_EXCEEDED", function() {
var res = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 1, new MatrixError({
errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 5000
})
);
expect(res >= 500).toBe(true, "Didn't wait long enough.");
});
it("should give up after 5 attempts", function() {
var res = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 5, {}
);
expect(res).toBe(-1, "Didn't give up.");
});
it("should do exponential backoff", function() {
expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 1, {}
)).toEqual(2000);
expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 2, {}
)).toEqual(4000);
expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 3, {}
)).toEqual(8000);
expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 4, {}
)).toEqual(16000);
});
});
});
+84
View File
@@ -0,0 +1,84 @@
"use strict";
var sdk = require("../..");
var User = sdk.User;
var utils = require("../test-utils");
describe("User", function() {
var userId = "@alice:bar";
var user;
beforeEach(function() {
utils.beforeEach(this);
user = new User(userId);
});
describe("setPresenceEvent", function() {
var event = utils.mkEvent({
type: "m.presence", content: {
presence: "online",
user_id: userId,
displayname: "Alice",
last_active_ago: 1085,
avatar_url: "mxc://foo/bar"
}, event: true
});
it("should emit 'User.displayName' if the display name changes", function() {
var emitCount = 0;
user.on("User.displayName", function(ev, usr) {
emitCount += 1;
});
user.setPresenceEvent(event);
expect(emitCount).toEqual(1);
user.setPresenceEvent(event); // no-op
expect(emitCount).toEqual(1);
});
it("should emit 'User.avatarUrl' if the avatar URL changes", function() {
var emitCount = 0;
user.on("User.avatarUrl", function(ev, usr) {
emitCount += 1;
});
user.setPresenceEvent(event);
expect(emitCount).toEqual(1);
user.setPresenceEvent(event); // no-op
expect(emitCount).toEqual(1);
});
it("should emit 'User.presence' if the presence changes", function() {
var emitCount = 0;
user.on("User.presence", function(ev, usr) {
emitCount += 1;
});
user.setPresenceEvent(event);
expect(emitCount).toEqual(1);
user.setPresenceEvent(event); // no-op
expect(emitCount).toEqual(1);
});
it("should set User.displayName", function() {
user.setPresenceEvent(event);
expect(user.displayName).toEqual("Alice");
});
it("should set User.avatarUrl", function() {
user.setPresenceEvent(event);
expect(user.avatarUrl).toEqual("mxc://foo/bar");
});
it("should set User.presence", function() {
user.setPresenceEvent(event);
expect(user.presence).toEqual("online");
});
it("should set User.lastActiveAgo", function() {
user.setPresenceEvent(event);
expect(user.lastActiveAgo).toEqual(1085);
});
it("should set User.events.presence", function() {
user.setPresenceEvent(event);
expect(user.events.presence).toEqual(event);
});
});
});
+135
View File
@@ -0,0 +1,135 @@
"use strict";
var utils = require("../../lib/utils");
var testUtils = require("../test-utils");
describe("utils", function() {
beforeEach(function() {
testUtils.beforeEach(this);
});
describe("encodeParams", function() {
it("should url encode and concat with &s", function() {
var params = {
foo: "bar",
baz: "beer@"
};
expect(utils.encodeParams(params)).toEqual(
"foo=bar&baz=beer%40"
);
});
});
describe("encodeUri", function() {
it("should replace based on object keys and url encode", function() {
var path = "foo/bar/%something/%here";
var vals = {
"%something": "baz",
"%here": "beer@"
};
expect(utils.encodeUri(path, vals)).toEqual(
"foo/bar/baz/beer%40"
);
});
});
describe("forEach", function() {
it("should be invoked for each element", function() {
var arr = [];
utils.forEach([55, 66, 77], function(element) {
arr.push(element);
});
expect(arr).toEqual([55, 66, 77]);
});
});
describe("findElement", function() {
it("should find only 1 element if there is a match", function() {
var matchFn = function() { return true; };
var arr = [55, 66, 77];
expect(utils.findElement(arr, matchFn)).toEqual(55);
});
it("should be able to find in reverse order", function() {
var matchFn = function() { return true; };
var arr = [55, 66, 77];
expect(utils.findElement(arr, matchFn, true)).toEqual(77);
});
it("should find nothing if the function never returns true", function() {
var matchFn = function() { return false; };
var arr = [55, 66, 77];
expect(utils.findElement(arr, matchFn)).toBeFalsy();
});
});
describe("removeElement", function() {
it("should remove only 1 element if there is a match", function() {
var matchFn = function() { return true; };
var arr = [55, 66, 77];
utils.removeElement(arr, matchFn);
expect(arr).toEqual([66, 77]);
});
it("should be able to remove in reverse order", function() {
var matchFn = function() { return true; };
var arr = [55, 66, 77];
utils.removeElement(arr, matchFn, true);
expect(arr).toEqual([55, 66]);
});
it("should remove nothing if the function never returns true", function() {
var matchFn = function() { return false; };
var arr = [55, 66, 77];
utils.removeElement(arr, matchFn);
expect(arr).toEqual(arr);
});
});
describe("isFunction", function() {
it("should return true for functions", function() {
expect(utils.isFunction([])).toBe(false);
expect(utils.isFunction([5, 3, 7])).toBe(false);
expect(utils.isFunction()).toBe(false);
expect(utils.isFunction(null)).toBe(false);
expect(utils.isFunction({})).toBe(false);
expect(utils.isFunction("foo")).toBe(false);
expect(utils.isFunction(555)).toBe(false);
expect(utils.isFunction(function() {})).toBe(true);
var s = { foo: function() {} };
expect(utils.isFunction(s.foo)).toBe(true);
});
});
describe("isArray", function() {
it("should return true for arrays", function() {
expect(utils.isArray([])).toBe(true);
expect(utils.isArray([5, 3, 7])).toBe(true);
expect(utils.isArray()).toBe(false);
expect(utils.isArray(null)).toBe(false);
expect(utils.isArray({})).toBe(false);
expect(utils.isArray("foo")).toBe(false);
expect(utils.isArray(555)).toBe(false);
expect(utils.isArray(function() {})).toBe(false);
});
});
describe("checkObjectHasKeys", function() {
it("should throw for missing keys", function() {
expect(function() { utils.checkObjectHasKeys({}, ["foo"]); }).toThrow();
expect(function() { utils.checkObjectHasKeys({
foo: "bar"
}, ["foo"]); }).not.toThrow();
});
});
describe("checkObjectHasNoAdditionalKeys", function() {
it("should throw for extra keys", function() {
expect(function() { utils.checkObjectHasNoAdditionalKeys({
foo: "bar",
baz: 4
}, ["foo"]); }).toThrow();
expect(function() { utils.checkObjectHasNoAdditionalKeys({
foo: "bar"
}, ["foo"]); }).not.toThrow();
});
});
});
+609
View File
@@ -0,0 +1,609 @@
"use strict";
var sdk = require("../..");
var WebStorageStore = sdk.WebStorageStore;
var Room = sdk.Room;
var User = sdk.User;
var utils = require("../test-utils");
function MockStorageApi() {
this.data = {};
this.keys = [];
this.length = 0;
}
MockStorageApi.prototype = {
setItem: function(k, v) {
this.data[k] = v;
this._recalc();
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
this._recalc();
},
key: function(index) {
return this.keys[index];
},
_recalc: function() {
var keys = [];
for (var k in this.data) {
if (!this.data.hasOwnProperty(k)) { continue; }
keys.push(k);
}
this.keys = keys;
this.length = keys.length;
}
};
describe("WebStorageStore", function() {
var store, room;
var roomId = "!foo:bar";
var userId = "@alice:bar";
var mockStorageApi;
var batchNum = 3;
// web storage api keys
var prefix = "room_" + roomId + "_timeline_";
var stateKeyName = "room_" + roomId + "_state";
// stored state events
var stateEventMap = {
"m.room.member": {},
"m.room.name": {}
};
stateEventMap["m.room.member"][userId] = utils.mkMembership(
{user: userId, room: roomId, mship: "join"}
);
stateEventMap["m.room.name"][""] = utils.mkEvent(
{user: userId, room: roomId, type: "m.room.name",
content: {
name: "foo"
}}
);
beforeEach(function() {
utils.beforeEach(this);
mockStorageApi = new MockStorageApi();
store = new WebStorageStore(mockStorageApi, batchNum);
room = new Room(roomId);
});
describe("constructor", function() {
it("should throw if the WebStorage API functions are missing", function() {
expect(function() {
store = new WebStorageStore({}, 5);
}).toThrow();
expect(function() {
mockStorageApi.length = undefined;
store = new WebStorageStore(mockStorageApi, 5);
}).toThrow();
});
});
describe("syncToken", function() {
it("get: should return the token from the store", function() {
var token = "flibble";
store.setSyncToken(token);
expect(store.getSyncToken()).toEqual(token);
expect(mockStorageApi.length).toEqual(1);
});
it("get: should return null if the token does not exist", function() {
expect(store.getSyncToken()).toEqual(null);
expect(mockStorageApi.length).toEqual(0);
});
});
describe("storeRoom", function() {
it("should persist the room state correctly", function() {
var stateEvents = [
utils.mkEvent({
event: true, type: "m.room.create", user: userId, room: roomId,
content: {
creator: userId
}
}),
utils.mkMembership({
event: true, user: userId, room: roomId, mship: "join"
})
];
room.currentState.setStateEvents(stateEvents);
store.storeRoom(room);
var storedEvents = getItem(mockStorageApi,
"room_" + roomId + "_state"
).events;
expect(storedEvents["m.room.create"][""]).toEqual(stateEvents[0].event);
});
it("should persist timeline events correctly", function() {
var timelineEvents = [];
var entries = batchNum + batchNum - 1;
var i = 0;
for (i = 0; i < entries; i++) {
timelineEvents.push(
utils.mkMessage({room: roomId, user: userId, event: true})
);
}
room.timeline = timelineEvents;
store.storeRoom(room);
expect(getItem(mockStorageApi, prefix + "-1")).toBe(null);
expect(getItem(mockStorageApi, prefix + "2")).toBe(null);
expect(getItem(mockStorageApi, prefix + "live")).toBe(null);
var timeline0 = getItem(mockStorageApi, prefix + "0");
var timeline1 = getItem(mockStorageApi, prefix + "1");
expect(timeline0.length).toEqual(batchNum);
expect(timeline1.length).toEqual(batchNum - 1);
for (i = 0; i < batchNum; i++) {
expect(timeline0[i]).toEqual(timelineEvents[i].event);
if ((i + batchNum) < timelineEvents.length) {
expect(timeline1[i]).toEqual(timelineEvents[i + batchNum].event);
}
}
});
it("should persist timeline events in one bucket if batchNum=0", function() {
store = new WebStorageStore(mockStorageApi, 0);
var timelineEvents = [];
var entries = batchNum + batchNum - 1;
var i = 0;
for (i = 0; i < entries; i++) {
timelineEvents.push(
utils.mkMessage({room: roomId, user: userId, event: true})
);
}
room.timeline = timelineEvents;
store.storeRoom(room);
expect(getItem(mockStorageApi, prefix + "-1")).toBe(null);
expect(getItem(mockStorageApi, prefix + "1")).toBe(null);
expect(getItem(mockStorageApi, prefix + "live")).toBe(null);
var timeline = getItem(mockStorageApi, prefix + "0");
expect(timeline.length).toEqual(timelineEvents.length);
for (i = 0; i < timeline.length; i++) {
expect(timeline[i]).toEqual(
timelineEvents[i].event
);
}
});
});
describe("getRoom", function() {
// stored timeline events
var timeline0, timeline1, i;
beforeEach(function() {
timeline0 = [];
timeline1 = [];
for (i = 0; i < batchNum; i++) {
timeline1[i] = utils.mkMessage({user: userId, room: roomId});
if (i !== (batchNum - 1)) { // miss last one
timeline0[i] = utils.mkMessage({user: userId, room: roomId});
}
}
});
it("should reconstruct room state", function() {
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
var storedRoom = store.getRoom(roomId);
expect(
storedRoom.currentState.getStateEvents("m.room.name", "").event
).toEqual(stateEventMap["m.room.name"][""]);
expect(
storedRoom.currentState.getStateEvents("m.room.member", userId).event
).toEqual(stateEventMap["m.room.member"][userId]);
});
it("should reconstruct old room state", function() {
var inviteEvent = utils.mkMembership({
user: userId, room: roomId, mship: "invite"
});
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", [inviteEvent]);
var storedRoom = store.getRoom(roomId);
expect(
storedRoom.currentState.getStateEvents("m.room.member", userId).event
).toEqual(stateEventMap["m.room.member"][userId]);
expect(
storedRoom.oldState.getStateEvents("m.room.member", userId).event
).toEqual(inviteEvent);
});
it("should reconstruct the room timeline", function() {
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
setItem(mockStorageApi, prefix + "1", timeline1);
var storedRoom = store.getRoom(roomId);
expect(storedRoom).not.toBeNull();
// should only get up to the batch num timeline events
expect(storedRoom.timeline.length).toEqual(batchNum);
var timeline = timeline0.concat(timeline1);
for (i = 0; i < batchNum; i++) {
expect(storedRoom.timeline[batchNum - 1 - i].event).toEqual(
timeline[timeline.length - 1 - i]
);
}
});
it("should sync the timeline for 'live' events " +
"(full hi batch; 1+bit live batches)", function() {
// 1 and a bit events go into _live
var timelineLive = [];
timelineLive.push(utils.mkMessage({user: userId, room: roomId}));
for (i = 0; i < batchNum; i++) {
timelineLive.push(
utils.mkMessage({user: userId, room: roomId})
);
}
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
setItem(mockStorageApi, prefix + "1", timeline1);
setItem(mockStorageApi,
// deep copy the timeline via parse/stringify else items will
// be shift()ed from timelineLive and we can't compare!
prefix + "live", JSON.parse(JSON.stringify(timelineLive))
);
var storedRoom = store.getRoom(roomId);
expect(storedRoom).not.toBeNull();
// should only get up to the batch num timeline events (highest
// index of timelineLive is the newest message)
expect(storedRoom.timeline.length).toEqual(batchNum);
for (i = 0; i < batchNum; i++) {
expect(storedRoom.timeline[i].event).toEqual(
timelineLive[i + 1]
);
}
});
it("should sync the timeline for 'live' events " +
"(no low batch; 1 live batches)", function() {
var timelineLive = [];
for (i = 0; i < batchNum; i++) {
timelineLive.push(
utils.mkMessage({user: userId, room: roomId})
);
}
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", []);
setItem(mockStorageApi,
// deep copy the timeline via parse/stringify else items will
// be shift()ed from timelineLive and we can't compare!
prefix + "live", JSON.parse(JSON.stringify(timelineLive))
);
var storedRoom = store.getRoom(roomId);
expect(storedRoom).not.toBeNull();
// should only get up to the batch num timeline events (highest
// index of timelineLive is the newest message)
expect(storedRoom.timeline.length).toEqual(batchNum);
for (i = 0; i < batchNum; i++) {
expect(storedRoom.timeline[i].event).toEqual(
timelineLive[i]
);
}
});
it("should be able to reconstruct the timeline with negative indices",
function() {
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "-5", timeline0);
setItem(mockStorageApi, prefix + "-4", timeline1);
var timeline = timeline0.concat(timeline1);
var storedRoom = store.getRoom(roomId);
expect(storedRoom).not.toBeNull();
// should only get up to the batch num timeline events
expect(storedRoom.timeline.length).toEqual(batchNum);
for (i = 0; i < batchNum; i++) {
expect(storedRoom.timeline[batchNum - 1 - i].event).toEqual(
timeline[timeline.length - 1 - i]
);
}
});
it("should return null if the room doesn't exist", function() {
expect(store.getRoom("nothing")).toEqual(null);
});
it("should assign a storageToken to the Room", function() {
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
setItem(mockStorageApi, prefix + "1", timeline1);
var storedRoom = store.getRoom(roomId);
expect(storedRoom.storageToken).toBeDefined();
});
});
describe("scrollback", function() {
// stored timeline events
var timeline0, timeline1, timeline2;
beforeEach(function() {
// batch size is 3
store = new WebStorageStore(mockStorageApi, 3);
timeline0 = [
// _
utils.mkMessage({user: userId, room: roomId}), // 1 OLDEST
utils.mkMessage({user: userId, room: roomId}) // 2
];
timeline1 = [
utils.mkMessage({user: userId, room: roomId}), // 3
utils.mkMessage({user: userId, room: roomId}), // 4
utils.mkMessage({user: userId, room: roomId}) // 5
];
timeline2 = [
utils.mkMessage({user: userId, room: roomId}), // 6
utils.mkMessage({user: userId, room: roomId}), // 7
utils.mkMessage({user: userId, room: roomId}) // 8 NEWEST
];
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
setItem(mockStorageApi, prefix + "1", timeline1);
setItem(mockStorageApi, prefix + "2", timeline2);
});
it("should scroll back locally giving 'limit' events", function() {
var storedRoom = store.getRoom(roomId);
expect(storedRoom.timeline.length).toEqual(3);
var events = store.scrollback(storedRoom, 3);
expect(events.length).toEqual(3);
expect(events.reverse()).toEqual(timeline1);
});
it("should give less than 'limit' events near the end of the stored timeline",
function() {
var storedRoom = store.getRoom(roomId);
expect(storedRoom.timeline.length).toEqual(3);
var events = store.scrollback(storedRoom, 7);
expect(events.length).toEqual(5);
expect(events.reverse()).toEqual(timeline0.concat(timeline1));
});
it("should progressively give older messages the more times scrollback is called",
function() {
var events;
var storedRoom = store.getRoom(roomId);
expect(storedRoom.timeline.length).toEqual(3);
events = store.scrollback(storedRoom, 2);
expect(events.reverse()).toEqual([timeline1[1], timeline1[2]]);
expect(storedRoom.timeline.length).toEqual(5);
events = store.scrollback(storedRoom, 2);
expect(events.reverse()).toEqual([timeline0[1], timeline1[0]]);
expect(storedRoom.timeline.length).toEqual(7);
events = store.scrollback(storedRoom, 2);
expect(events).toEqual([timeline0[0]]);
expect(storedRoom.timeline.length).toEqual(8);
events = store.scrollback(storedRoom, 2);
expect(events).toEqual([]);
expect(storedRoom.timeline.length).toEqual(8);
});
it("should give 0 events if there is no token on the room", function() {
var r = new Room(roomId);
expect(store.scrollback(r, 3)).toEqual([]);
});
it("should give 0 events for unknown rooms", function() {
var r = new Room("!unknown:room");
r.storageToken = "foo";
expect(store.scrollback(r, 3)).toEqual([]);
});
it("should give 0 events if the boundary event is the last in the timeline",
function() {
var events;
var storedRoom = store.getRoom(roomId);
expect(storedRoom.timeline.length).toEqual(3);
// go up to the boundary (8 messages total)
events = store.scrollback(storedRoom, 5);
expect(events.length).toEqual(5);
events = store.scrollback(storedRoom, 5);
expect(events.length).toEqual(0);
});
});
describe("storeEvents", function() {
var timeline0, i;
beforeEach(function() {
timeline0 = [];
for (i = 0; i < batchNum; i++) {
timeline0.push(utils.mkMessage({user: userId, room: roomId}));
}
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
});
it("should add to the live batch", function() {
var events = [
utils.mkMessage({user: userId, room: roomId, event: true}),
utils.mkMessage({user: userId, room: roomId, event: true})
];
store.storeEvents(room, events, "atoken");
var liveEvents = getItem(mockStorageApi, prefix + "live");
expect(liveEvents.length).toEqual(2);
expect(liveEvents[0]).toEqual(events[0].event);
expect(liveEvents[1]).toEqual(events[1].event);
});
it("should preserve existing live events in the store", function() {
var existingEvent = utils.mkMessage({user: userId, room: roomId});
setItem(mockStorageApi, prefix + "live", [existingEvent]);
var events = [
utils.mkMessage({user: userId, room: roomId, event: true}),
utils.mkMessage({user: userId, room: roomId, event: true})
];
store.storeEvents(room, events, "atoken");
var liveEvents = getItem(mockStorageApi, prefix + "live");
expect(liveEvents.length).toEqual(3);
expect(liveEvents[0]).toEqual(existingEvent);
expect(liveEvents[1]).toEqual(events[0].event);
expect(liveEvents[2]).toEqual(events[1].event);
});
it("should add to the lowest batch index if toStart=true", function() {
var events = [
utils.mkMessage({user: userId, room: roomId, event: true}),
utils.mkMessage({user: userId, room: roomId, event: true})
];
store.storeEvents(room, events, "atoken", true);
var timelineNeg1 = getItem(mockStorageApi, prefix + "-1");
expect(timelineNeg1.length).toEqual(2);
expect(timelineNeg1[0]).toEqual(events[1].event);
expect(timelineNeg1[1]).toEqual(events[0].event);
});
it("should add multiple batches to the lowest batch index if toStart=true",
function() {
var timelineNeg1 = [];
var timelineNeg2 = [];
for (i = 0; i < batchNum; i++) {
timelineNeg1.push(
utils.mkMessage({user: userId, room: roomId, event: true})
);
timelineNeg2.push(
utils.mkMessage({user: userId, room: roomId, event: true})
);
}
var events = timelineNeg2.concat(timelineNeg1).reverse();
store.storeEvents(room, events, "atoken", true);
var storedNeg1 = getItem(mockStorageApi, prefix + "-1");
var storedNeg2 = getItem(mockStorageApi, prefix + "-2");
expect(timelineNeg1.length).toEqual(storedNeg1.length);
expect(timelineNeg2.length).toEqual(storedNeg2.length);
for (i = 0; i < timelineNeg1.length; i++) {
expect(timelineNeg1[i].event).toEqual(storedNeg1[i]);
expect(timelineNeg2[i].event).toEqual(storedNeg2[i]);
}
});
it("should update stored state if state events exist", function() {
var events = [
utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", event: true,
content: {
name: "Room Name Here for updates"
}
})
];
room.currentState.setStateEvents(events);
store.storeEvents(room, events, "atoken");
var liveEvents = getItem(mockStorageApi, prefix + "live");
expect(liveEvents.length).toEqual(1);
expect(liveEvents[0]).toEqual(events[0].event);
var stateEvents = getItem(mockStorageApi, stateKeyName);
expect(stateEvents.events["m.room.name"][""]).toEqual(events[0].event);
});
});
describe("getRooms", function() {
var mkState = function(id) {
return [
utils.mkEvent({
event: true, type: "m.room.create", user: userId, room: id,
content: {
creator: userId
}
}),
utils.mkMembership({
event: true, user: userId, room: id, mship: "join"
})
];
};
it("should get all rooms in the store", function() {
var roomIds = [
"!alpha:bet", "!beta:fet"
];
// store 2 dynamically
var roomA = new Room(roomIds[0]);
roomA.currentState.setStateEvents(mkState(roomIds[0]));
var roomB = new Room(roomIds[1]);
roomB.currentState.setStateEvents(mkState(roomIds[1]));
store.storeRoom(roomA);
store.storeRoom(roomB);
var rooms = store.getRooms();
expect(rooms.length).toEqual(2);
for (var i = 0; i < rooms.length; i++) {
var index = roomIds.indexOf(rooms[i].roomId);
expect(index).not.toEqual(
-1, "Unknown room"
);
roomIds.splice(index, 1);
}
});
});
describe("getUser", function() {
it("should be able to retrieve a stored user", function() {
var user = new User(userId);
store.storeUser(user);
var result = store.getUser(userId);
expect(result).toBeDefined();
expect(result.userId).toEqual(userId);
});
it("should be able to retrieve a stored user with name data", function() {
var presence = utils.mkEvent({
type: "m.presence", event: true, content: {
user_id: userId,
displayname: "Flibble"
}
});
var user = new User(userId);
user.setPresenceEvent(presence);
store.storeUser(user);
var result = store.getUser(userId);
console.log(result);
expect(result.events.presence).toEqual(presence);
});
});
});
function getItem(store, key) {
return JSON.parse(store.getItem(key));
}
function setItem(store, key, val) {
store.setItem(key, JSON.stringify(val));
}