diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bd8fb86 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "cSpell.words": [ + "errcode", + "reqwest" + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index e48c706..6bd7641 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,8 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +url = "2.1" +reqwest = "0.9" +serde_json = "1.0" +serde = "1.0" +olm-rs = "0.2" diff --git a/docs/todo.md b/docs/todo.md new file mode 100644 index 0000000..fa70727 --- /dev/null +++ b/docs/todo.md @@ -0,0 +1,442 @@ +# r0.5 + +- [ ] 2 API Standards + - [ ] 2.1 GET /_matrix/client/versions +- [x] ~~3 Web Browser Clients~~ *Not applicable* +- [ ] 4 Server Discovery + - [ ] 4.1 Well-known URI + - [ ] 4.1.1 GET /.well-known/matrix/client +- [ ] 5 Client Authentication + - [ ] 5.1 Using access tokens + - [ ] 5.2 Relationship between access tokens and devices + - [ ] 5.3 User-Interactive Authentication API + - [ ] 5.3.1 Overview + - [ ] 5.3.2 User-interactive API in the REST API + - [ ] 5.3.3 Example + - [ ] 5.3.4 Authentication types + - [ ] 5.3.4.1 Password-based + - [ ] 5.3.4.2 Google ReCaptcha + - [ ] 5.3.4.3 Token-based + - [ ] 5.3.4.4 OAuth2-based + - [ ] 5.3.4.5 Email-based (identity / homeserver) + - [ ] 5.3.4.6 Phone number/MSISDN-based (identity / homeserver) + - [ ] 5.3.4.7 Dummy Auth + - [ ] 5.3.5 Fallback + - [ ] 5.3.5.1 Example + - [ ] 5.3.6 Identifier types + - [ ] 5.3.6.1 Matrix User ID + - [ ] 5.3.6.2 Third-party ID + - [ ] 5.3.6.3 Phone number + - [ ] 5.4 Login + - [ ] 5.4.1 GET /_matrix/client/r0/login + - [ ] 5.4.2 POST /_matrix/client/r0/login + - [ ] 5.4.3 POST /_matrix/client/r0/logout + - [ ] 5.4.4 POST /_matrix/client/r0/logout/all + - [ ] 5.4.5 Login Fallback + - [ ] 5.5 Account registration and management + - [ ] 5.5.1 POST /_matrix/client/r0/register + - [ ] 5.5.2 POST /_matrix/client/r0/register/email/requestToken + - [ ] 5.5.3 POST /_matrix/client/r0/register/msisdn/requestToken + - [ ] 5.5.4 POST /_matrix/client/r0/account/password + - [ ] 5.5.5 POST /_matrix/client/r0/account/password/email/requestToken + - [ ] 5.5.6 POST /_matrix/client/r0/account/password/msisdn/requestToken + - [ ] 5.5.7 POST /_matrix/client/r0/account/deactivate + - [ ] 5.5.8 GET /_matrix/client/r0/register/available + - [ ] 5.5.9 Notes on password management + - [ ] 5.6 Adding Account Administrative Contact Information + - [ ] 5.6.1 GET /_matrix/client/r0/account/3pid + - [ ] 5.6.2 POST /_matrix/client/r0/account/3pid + - [ ] 5.6.3 POST /_matrix/client/r0/account/3pid/delete + - [ ] 5.6.4 POST /_matrix/client/r0/account/3pid/email/requestToken + - [ ] 5.6.5 POST /_matrix/client/r0/account/3pid/msisdn/requestToken + - [ ] 5.7 Current account information + - [ ] 5.7.1 GET /_matrix/client/r0/account/whoami +- [ ] 6 Capabilities negotiation + - [ ] 6.1 GET /_matrix/client/r0/capabilities + - [ ] 6.2 m.change_password capability + - [ ] 6.3 m.room_versions capability +- [ ] 7 Pagination +- [ ] 8 Filtering + - [ ] 8.1 Lazy-loading room members + - [ ] 8.2 API endpoints + - [ ] 8.2.1 POST /_matrix/client/r0/user/{userId}/filter + - [ ] 8.2.2 GET /_matrix/client/r0/user/{userId}/filter/{filterId} +- [ ] 9 Events + - [ ] 9.1 Types of room events + - [ ] 9.1.1 Event Fields + - [ ] 9.1.2 Room Event Fields + - [ ] 9.1.3 State Event Fields + - [ ] 9.2 Size limits + - [ ] 9.3 Room Events + - [ ] 9.3.1 m.room.aliases + - [ ] 9.3.2 m.room.canonical_alias + - [ ] 9.3.3 m.room.create + - [ ] 9.3.4 m.room.join_rules + - [ ] 9.3.5 m.room.member + - [ ] 9.3.6 m.room.power_levels + - [ ] 9.3.7 m.room.redaction + - [x] 9.4 Syncing + - [x] 9.4.1 GET /_matrix/client/r0/sync + - [x] ~~9.4.2 GET /_matrix/client/r0/events~~ *Deprecated* + - [x] ~~9.4.3 GET /_matrix/client/r0/initialSync~~ *Deprecated* + - [x] ~~9.4.4 GET /_matrix/client/r0/events/{eventId}~~ *Deprecated* + - [ ] 9.5 Getting events for a room + - [ ] 9.5.1 GET /_matrix/client/r0/rooms/{roomId}/event/{eventId} + - [ ] 9.5.2 GET /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey} + - [ ] 9.5.3 GET /_matrix/client/r0/rooms/{roomId}/state + - [ ] 9.5.4 GET /_matrix/client/r0/rooms/{roomId}/members + - [ ] 9.5.5 GET /_matrix/client/r0/rooms/{roomId}/joined_members + - [ ] 9.5.6 GET /_matrix/client/r0/rooms/{roomId}/messages + - [ ] 9.5.7 GET /_matrix/client/r0/rooms/{roomId}/initialSync + - [ ] 9.6 Sending events to a room + - [ ] 9.6.1 PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey} + - [ ] 9.6.2 PUT /_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId} + - [ ] 9.7 Redactions + - [ ] 9.7.1 Events + - [ ] 9.7.1.1 m.room.redaction + - [ ] 9.7.2 Client behaviour + - [ ] 9.7.2.1 PUT /_matrix/client/r0/rooms/{roomId}/redact/{eventId}/{txnId} +- [ ] 10 Rooms + - [ ] 10.1 Creation + - [ ] 10.1.1 POST /_matrix/client/r0/createRoom + - [ ] 10.2 Room aliases + - [ ] 10.2.1 PUT /_matrix/client/r0/directory/room/{roomAlias} + - [ ] 10.2.2 GET /_matrix/client/r0/directory/room/{roomAlias} + - [ ] 10.2.3 DELETE /_matrix/client/r0/directory/room/{roomAlias} + - [ ] 10.3 Permissions + - [ ] 10.4 Room membership + - [ ] 10.4.1 GET /_matrix/client/r0/joined_rooms + - [ ] 10.4.2 Joining rooms + - [ ] 10.4.2.1 POST /_matrix/client/r0/rooms/{roomId}/invite + - [ ] 10.4.2.2 POST /_matrix/client/r0/rooms/{roomId}/join + - [ ] 10.4.2.3 POST /_matrix/client/r0/join/{roomIdOrAlias} + - [ ] 10.4.3 Leaving rooms + - [ ] 10.4.3.1 POST /_matrix/client/r0/rooms/{roomId}/leave + - [ ] 10.4.3.2 POST /_matrix/client/r0/rooms/{roomId}/forget + - [ ] 10.4.3.3 POST /_matrix/client/r0/rooms/{roomId}/kick + - [ ] 10.4.4 Banning users in a room + - [ ] 10.4.4.1 POST /_matrix/client/r0/rooms/{roomId}/ban + - [ ] 10.4.4.2 POST /_matrix/client/r0/rooms/{roomId}/unban + - [ ] 10.5 Listing rooms + - [ ] 10.5.1 GET /_matrix/client/r0/directory/list/room/{roomId} + - [ ] 10.5.2 PUT /_matrix/client/r0/directory/list/room/{roomId} + - [ ] 10.5.3 GET /_matrix/client/r0/publicRooms + - [ ] 10.5.4 POST /_matrix/client/r0/publicRooms +- [ ] 11 User Data + - [ ] 11.1 User Directory + - [ ] 11.1.1 POST /_matrix/client/r0/user_directory/search + - [ ] 11.2 Profiles + - [ ] 11.2.1 PUT /_matrix/client/r0/profile/{userId}/displayname + - [ ] 11.2.2 GET /_matrix/client/r0/profile/{userId}/displayname + - [ ] 11.2.3 PUT /_matrix/client/r0/profile/{userId}/avatar_url + - [ ] 11.2.4 GET /_matrix/client/r0/profile/{userId}/avatar_url + - [ ] 11.2.5 GET /_matrix/client/r0/profile/{userId} + - [ ] 11.2.6 Events on Change of Profile Information +- [ ] 12 Security + - [ ] 12.1 Rate limiting +- [ ] 13 Modules + - [ ] 13.1 Feature Profiles + - [ ] 13.1.1 Summary + - [ ] 13.1.2 Clients + - [ ] 13.1.2.1 Stand-alone web (Web) + - [ ] 13.1.2.2 Mobile (Mobile) + - [ ] 13.1.2.3 Desktop (Desktop) + - [ ] 13.1.2.4 Command Line Interface (CLI) + - [ ] 13.1.2.5 Embedded (Embedded) + - [ ] 13.1.2.5.1 Application + - [ ] 13.1.2.5.2 Device + - [ ] 13.2 Instant Messaging + - [ ] 13.2.1 Events + - [ ] 13.2.1.1 m.room.message + - [ ] 13.2.1.2 m.room.message.feedback + - [ ] 13.2.1.3 m.room.name + - [ ] 13.2.1.4 m.room.topic + - [ ] 13.2.1.5 m.room.avatar + - [ ] 13.2.1.6 m.room.pinned_events + - [ ] 13.2.1.7 m.room.message msgtypes + - [ ] 13.2.1.7.1 m.text + - [ ] 13.2.1.7.2 m.emote + - [ ] 13.2.1.7.3 m.notice + - [ ] 13.2.1.7.4 m.image + - [ ] 13.2.1.7.5 m.file + - [ ] 13.2.1.7.6 m.audio + - [ ] 13.2.1.7.7 m.location + - [ ] 13.2.1.7.8 m.video + - [ ] 13.2.2 Client behaviour + - [ ] 13.2.2.1 Recommendations when sending messages + - [ ] 13.2.2.2 Local echo + - [ ] 13.2.2.3 Calculating the display name for a user + - [ ] 13.2.2.4 Displaying membership information with messages + - [ ] 13.2.2.5 Calculating the display name for a room + - [ ] 13.2.2.6 Forming relationships between events + - [ ] 13.2.2.6.1 Rich replies + - [ ] 13.2.2.6.1.1 Fallbacks and event representation + - [ ] 13.2.2.6.1.1.1 Stripping the fallback + - [ ] 13.2.2.6.1.1.2 Fallback for m.text, m.notice, and unrecognised message types + - [ ] 13.2.2.6.1.1.3 Fallback for m.emote + - [ ] 13.2.2.6.1.1.4 Fallback for m.image, m.video, m.audio, and m.file + - [ ] 13.2.3 Server behaviour + - [ ] 13.2.4 Security considerations + - [ ] 13.3 Voice over IP + - [ ] 13.3.1 Events + - [ ] 13.3.1.1 m.call.invite + - [ ] 13.3.1.2 m.call.candidates + - [ ] 13.3.1.3 m.call.answer + - [ ] 13.3.1.4 m.call.hangup + - [ ] 13.3.2 Client behaviour + - [ ] 13.3.2.1 Glare + - [ ] 13.3.3 Server behaviour + - [ ] 13.3.3.1 GET /_matrix/client/r0/voip/turnServer + - [ ] 13.3.4 Security considerations + - [ ] 13.4 Typing Notifications + - [ ] 13.4.1 Events + - [ ] 13.4.1.1 m.typing + - [ ] 13.4.2 Client behaviour + - [ ] 13.4.2.1 PUT /_matrix/client/r0/rooms/{roomId}/typing/{userId} + - [ ] 13.4.3 Security considerations + - [ ] 13.5 Receipts + - [ ] 13.5.1 Events + - [ ] 13.5.1.1 m.receipt + - [ ] 13.5.2 Client behaviour + - [ ] 13.5.2.1 POST /_matrix/client/r0/rooms/{roomId}/receipt/{receiptType}/{eventId} + - [ ] 13.5.3 Server behaviour + - [ ] 13.5.4 Security considerations + - [ ] 13.6 Fully read markers + - [ ] 13.6.1 Events + - [ ] 13.6.1.1 m.fully_read + - [ ] 13.6.2 Client behaviour + - [ ] 13.6.2.1 POST /_matrix/client/r0/rooms/{roomId}/read_markers + - [ ] 13.6.3 Server behaviour + - [ ] 13.7 Presence + - [ ] 13.7.1 Events + - [ ] 13.7.1.1 m.presence + - [ ] 13.7.2 Client behaviour + - [ ] 13.7.2.1 PUT /_matrix/client/r0/presence/{userId}/status + - [ ] 13.7.2.2 GET /_matrix/client/r0/presence/{userId}/status + - [ ] 13.7.2.3 Last active ago + - [ ] 13.7.2.4 Idle timeout + - [ ] 13.7.3 Security considerations + - [ ] 13.8 Content repository + - [ ] 13.8.1 Matrix Content (MXC) URIs + - [ ] 13.8.2 Client behaviour + - [ ] 13.8.2.1 POST /_matrix/media/r0/upload + - [ ] 13.8.2.2 GET /_matrix/media/r0/download/{serverName}/{mediaId} + - [ ] 13.8.2.3 GET /_matrix/media/r0/download/{serverName}/{mediaId}/{fileName} + - [ ] 13.8.2.4 GET /_matrix/media/r0/thumbnail/{serverName}/{mediaId} + - [ ] 13.8.2.5 GET /_matrix/media/r0/preview_url + - [ ] 13.8.2.6 GET /_matrix/media/r0/config + - [ ] 13.8.2.7 Thumbnails + - [ ] 13.8.3 Security considerations + - [ ] 13.9 Send-to-Device messaging + - [ ] 13.9.1 Client behaviour + - [ ] 13.9.2 Server behaviour + - [ ] 13.9.3 Protocol definitions + - [ ] 13.9.3.1 PUT /_matrix/client/r0/sendToDevice/{eventType}/{txnId} + - [ ] 13.9.3.2 Extensions to /sync + - [ ] 13.10 Device Management + - [ ] 13.10.1 Client behaviour + - [ ] 13.10.1.1 GET /_matrix/client/r0/devices + - [ ] 13.10.1.2 GET /_matrix/client/r0/devices/{deviceId} + - [ ] 13.10.1.3 PUT /_matrix/client/r0/devices/{deviceId} + - [ ] 13.10.1.4 DELETE /_matrix/client/r0/devices/{deviceId} + - [ ] 13.10.1.5 POST /_matrix/client/r0/delete_devices + - [ ] 13.10.2 Security considerations + - [ ] 13.11 End-to-End Encryption + - [ ] 13.11.1 Key Distribution + - [ ] 13.11.1.1 Overview + - [ ] 13.11.1.2 Key algorithms + - [ ] 13.11.1.3 Device keys + - [ ] 13.11.1.4 Uploading keys + - [ ] 13.11.1.5 Tracking the device list for a user + - [ ] 13.11.1.6 Sending encrypted attachments + - [ ] 13.11.1.6.1 Extensions to m.message msgtypes + - [ ] 13.11.1.7 Claiming one-time keys + - [ ] 13.11.2 Device verification + - [ ] 13.11.2.1 Key verification framework + - [ ] 13.11.2.1.1 m.key.verification.request + - [ ] 13.11.2.1.2 m.key.verification.start + - [ ] 13.11.2.1.3 m.key.verification.cancel + - [ ] 13.11.2.2 Short Authentication String (SAS) verification + - [ ] 13.11.2.2.1 Error and exception handling + - [ ] 13.11.2.2.2 Verification messages specific to SAS + - [ ] 13.11.2.2.3 m.key.verification.start + - [ ] 13.11.2.2.4 m.key.verification.accept + - [ ] 13.11.2.2.5 m.key.verification.key + - [ ] 13.11.2.2.6 m.key.verification.mac + - [ ] 13.11.2.2.7 HKDF calculation + - [ ] 13.11.2.2.8 SAS method: decimal + - [ ] 13.11.2.2.9 SAS method: emoji + - [ ] 13.11.3 Sharing keys between devices + - [ ] 13.11.3.1 Key requests + - [ ] 13.11.3.2 Key exports + - [ ] 13.11.3.2.1 Key export format + - [ ] 13.11.4 Messaging Algorithms + - [ ] 13.11.4.1 Messaging Algorithm Names + - [ ] 13.11.4.2 m.olm.v1.curve25519-aes-sha2 + - [ ] 13.11.4.2.1 Recovering from undecryptable messages + - [ ] 13.11.4.3 m.megolm.v1.aes-sha2 + - [ ] 13.11.5 Protocol definitions + - [ ] 13.11.5.1 Events + - [ ] 13.11.5.1.1 m.room.encryption + - [ ] 13.11.5.1.2 m.room.encrypted + - [ ] 13.11.5.1.3 m.room_key + - [ ] 13.11.5.1.4 m.room_key_request + - [ ] 13.11.5.1.5 m.forwarded_room_key + - [ ] 13.11.5.1.6 m.dummy + - [ ] 13.11.5.2 Key management API + - [ ] 13.11.5.2.1 POST /_matrix/client/r0/keys/upload + - [ ] 13.11.5.2.2 POST /_matrix/client/r0/keys/query + - [ ] 13.11.5.2.3 POST /_matrix/client/r0/keys/claim + - [ ] 13.11.5.2.4 GET /_matrix/client/r0/keys/changes + - [ ] 13.11.5.3 Extensions to /sync + - [ ] 13.12 Room History Visibility + - [ ] 13.12.1 Events + - [ ] 13.12.1.1 m.room.history_visibility + - [ ] 13.12.2 Client behaviour + - [ ] 13.12.3 Server behaviour + - [ ] 13.12.4 Security considerations + - [ ] 13.13 Push Notifications + - [ ] 13.13.1 Client behaviour + - [ ] 13.13.1.1 GET /_matrix/client/r0/pushers + - [ ] 13.13.1.2 POST /_matrix/client/r0/pushers/set + - [ ] 13.13.1.3 Listing Notifications + - [ ] 13.13.1.3.1 GET /_matrix/client/r0/notifications + - [ ] 13.13.1.4 Receiving notifications + - [ ] 13.13.1.5 Push Rules + - [ ] 13.13.1.5.1 Actions + - [ ] 13.13.1.5.1.1 Tweaks + - [ ] 13.13.1.5.2 Predefined Rules + - [ ] 13.13.1.5.2.1 Default Override Rules + - [ ] 13.13.1.5.2.1.1 .m.rule.master + - [ ] 13.13.1.5.2.1.2 .m.rule.suppress_notices + - [ ] 13.13.1.5.2.1.3 .m.rule.invite_for_me + - [ ] 13.13.1.5.2.1.4 .m.rule.member_event + - [ ] 13.13.1.5.2.1.5 .m.rule.contains_display_name + - [ ] 13.13.1.5.2.1.6 .m.rule.tombstone + - [ ] 13.13.1.5.2.1.7 .m.rule.roomnotif + - [ ] 13.13.1.5.2.2 Default Content Rules + - [ ] 13.13.1.5.2.2.1 .m.rule.contains_user_name + - [ ] 13.13.1.5.2.3 Default Underride Rules + - [ ] 13.13.1.5.2.3.1 .m.rule.call + - [ ] 13.13.1.5.2.3.2 .m.rule.encrypted_room_one_to_one + - [ ] 13.13.1.5.2.3.3 .m.rule.room_one_to_one + - [ ] 13.13.1.5.2.3.4 .m.rule.message + - [ ] 13.13.1.5.2.3.5 .m.rule.encrypted + - [ ] 13.13.1.5.3 Conditions + - [ ] 13.13.1.6 Push Rules: API + - [ ] 13.13.1.6.1 GET /_matrix/client/r0/pushrules/ + - [ ] 13.13.1.6.2 GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId} + - [ ] 13.13.1.6.3 DELETE /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId} + - [ ] 13.13.1.6.4 PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId} + - [ ] 13.13.1.6.5 GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled + - [ ] 13.13.1.6.6 PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled + - [ ] 13.13.1.6.7 GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions + - [ ] 13.13.1.6.8 PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions + - [ ] 13.13.1.7 Push Rules: Events + - [ ] 13.13.1.7.1 m.push_rules + - [ ] 13.13.1.7.2 Examples + - [ ] 13.13.2 Server behaviour + - [ ] 13.13.3 Push Gateway behaviour + - [ ] 13.13.3.1 Recommendations for APNS + - [ ] 13.13.4 Security considerations + - [ ] 13.14 Third party invites + - [ ] 13.14.1 Events + - [ ] 13.14.1.1 m.room.third_party_invite + - [ ] 13.14.2 Client behaviour + - [ ] 13.14.2.1 POST /_matrix/client/r0/rooms/{roomId}/invite + - [ ] 13.14.3 Server behaviour + - [ ] 13.14.4 Security considerations + - [ ] 13.15 Server Side Search + - [ ] 13.15.1 Client behaviour + - [ ] 13.15.1.1 POST /_matrix/client/r0/search + - [ ] 13.15.2 Search Categories + - [ ] 13.15.2.1 room_events + - [ ] 13.15.3 Ordering + - [ ] 13.15.4 Groups + - [ ] 13.15.5 Pagination + - [ ] 13.15.6 Security considerations + - [ ] 13.16 Guest Access + - [ ] 13.16.1 Events + - [ ] 13.16.1.1 m.room.guest_access + - [ ] 13.16.2 Client behaviour + - [ ] 13.16.3 Server behaviour + - [ ] 13.16.4 Security considerations + - [ ] 13.17 Room Previews + - [ ] 13.17.1 Client behaviour + - [ ] 13.17.1.1 GET /_matrix/client/r0/events + - [ ] 13.17.2 Server behaviour + - [ ] 13.17.3 Security considerations + - [ ] 13.18 Room Tagging + - [ ] 13.18.1 Events + - [ ] 13.18.1.1 m.tag + - [ ] 13.18.2 Client Behaviour + - [ ] 13.18.2.1 GET /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags + - [ ] 13.18.2.2 PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag} + - [ ] 13.18.2.3 DELETE /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag} + - [ ] 13.19 Client Config + - [ ] 13.19.1 Events + - [ ] 13.19.2 Client Behaviour + - [ ] 13.19.2.1 PUT /_matrix/client/r0/user/{userId}/account_data/{type} + - [ ] 13.19.2.2 GET /_matrix/client/r0/user/{userId}/account_data/{type} + - [ ] 13.19.2.3 PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type} + - [ ] 13.19.2.4 GET /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type} + - [ ] 13.19.3 Server Behaviour + - [ ] 13.20 Server Administration + - [ ] 13.20.1 Client Behaviour + - [ ] 13.20.1.1 GET /_matrix/client/r0/admin/whois/{userId} + - [ ] 13.21 Event Context + - [ ] 13.21.1 Client behaviour + - [ ] 13.21.1.1 GET /_matrix/client/r0/rooms/{roomId}/context/{eventId} + - [ ] 13.21.2 Security considerations + - [ ] 13.22 SSO client login + - [ ] 13.22.1 Client behaviour + - [ ] 13.22.1.1 GET /_matrix/client/r0/login/sso/redirect + - [ ] 13.22.2 Server behaviour + - [ ] 13.22.2.1 Handling the redirect endpoint + - [ ] 13.22.2.2 Handling the authentication endpoint + - [ ] 13.23 Direct Messaging + - [ ] 13.23.1 Events + - [ ] 13.23.1.1 m.direct + - [ ] 13.23.2 Client behaviour + - [ ] 13.23.3 Server behaviour + - [ ] 13.24 Ignoring Users + - [ ] 13.24.1 Events + - [ ] 13.24.1.1 m.ignored_user_list + - [ ] 13.24.2 Client behaviour + - [ ] 13.24.3 Server behaviour + - [ ] 13.25 Sticker Messages + - [ ] 13.25.1 Events + - [ ] 13.25.1.1 m.sticker + - [ ] 13.25.2 Client behaviour + - [ ] 13.26 Reporting Content + - [ ] 13.26.1 Client behaviour + - [ ] 13.26.1.1 POST /_matrix/client/r0/rooms/{roomId}/report/{eventId} + - [ ] 13.26.2 Server behaviour + - [ ] 13.27 Third Party Networks + - [ ] 13.27.1 Third Party Lookups + - [ ] 13.27.1.1 GET /_matrix/client/r0/thirdparty/protocols + - [ ] 13.27.1.2 GET /_matrix/client/r0/thirdparty/protocol/{protocol} + - [ ] 13.27.1.3 GET /_matrix/client/r0/thirdparty/location/{protocol} + - [ ] 13.27.1.4 GET /_matrix/client/r0/thirdparty/user/{protocol} + - [ ] 13.27.1.5 GET /_matrix/client/r0/thirdparty/location + - [ ] 13.27.1.6 GET /_matrix/client/r0/thirdparty/user + - [ ] 13.28 OpenID + - [ ] 13.28.1 POST /_matrix/client/r0/user/{userId}/openid/request_token + - [ ] 13.29 Server Access Control Lists (ACLs) for rooms + - [ ] 13.29.1 m.room.server_acl + - [ ] 13.29.2 Client behaviour + - [ ] 13.29.3 Server behaviour + - [ ] 13.29.4 Security considerations + - [ ] 13.30 User, room, and group mentions + - [ ] 13.30.1 Client behaviour + - [ ] 13.31 Room Upgrades + - [ ] 13.31.1 Events + - [ ] 13.31.1.1 m.room.tombstone + - [ ] 13.31.2 Client behaviour + - [ ] 13.31.2.1 POST /_matrix/client/r0/rooms/{roomId}/upgrade + - [ ] 13.31.3 Server behaviour + - [ ] 13.32 Server Notices + - [ ] 13.32.1 Events + - [ ] 13.32.1.1 m.room.message (m.server_notice) + - [ ] 13.32.2 Client behaviour + - [ ] 13.32.3 Server behaviour diff --git a/src/api/client.rs b/src/api/client.rs new file mode 100644 index 0000000..ac52e26 --- /dev/null +++ b/src/api/client.rs @@ -0,0 +1,231 @@ +use crate::api::methods::sync::SyncResponse; +use reqwest::Client as reqwest_client; +use reqwest::{ + header::{HeaderMap, HeaderValue, CONTENT_TYPE, USER_AGENT}, + StatusCode, +}; +use std::{collections::HashMap, error::Error, fmt, time}; +use url::{ParseError, Url}; + +const V2_API_PATH: &str = "/_matrix/client/r0"; + +#[derive(Debug)] +pub enum MatrixParseError { + ParseError(ParseError), + EmptyScheme, +} + +pub enum PresenceState { + Offline, + Online, + Unavailable, +} + +impl From for MatrixParseError { + fn from(parse_error: ParseError) -> Self { + MatrixParseError::ParseError(parse_error) + } +} + +#[derive(Debug)] +pub struct ResponseError { + code: StatusCode, + content: String, +} + +impl fmt::Display for ResponseError { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "{}: {}", self.code, self.content) + } +} + +impl Error for ResponseError {} + +pub enum MatrixHTTPMethod { + Get, + Put, + Delete, + Post, +} + +#[derive(Debug)] +pub struct Client { + homeserver_url: String, + access_token: Option, + mxid: Option, + default_492_wait_ms: u64, + use_auth_header: bool, + reqwest_client: reqwest_client, +} + +impl Client { + pub fn new( + homeserver_url: &str, + access_token: Option, + mxid: Option, + default_492_wait_ms: Option, + use_auth_header: Option, + ) -> Result { + let url = Url::parse(homeserver_url)?; + if url.scheme().is_empty() { + return Err(MatrixParseError::EmptyScheme); + } + + Ok(Client { + homeserver_url: homeserver_url.to_string(), + access_token, + mxid, + default_492_wait_ms: default_492_wait_ms.unwrap_or_else(|| 5000), + use_auth_header: use_auth_header.unwrap_or_else(|| true), + reqwest_client: reqwest_client::new(), + }) + } + + /// Sends an API request to the homeserver using the specified method and + /// path, returning either an error or the response text. + /// + /// The header will automatically be populated with a user agent and have + /// the content type set to `application/json`. If a token was provided, it + /// will be used as a bearer auth header or as a query. + /// + /// This is a blocking, synchronous send. If the response from the + /// homeserver indicates that too many requests were sent, it will attempt + /// to wait the specified duration (or a provided default) before retrying. + fn send( + &self, + method: MatrixHTTPMethod, + path: Option<&str>, + content: Option, + query_params: Option>, + headers: Option, + ) -> Result> { + let mut query_params = query_params.unwrap_or_default(); + let mut headers = headers.unwrap_or_default(); + let endpoint = &format!( + "{}{}", + self.homeserver_url, + path.unwrap_or_else(|| V2_API_PATH), + ); + let mut request = match method { + MatrixHTTPMethod::Get => self.reqwest_client.get(endpoint), + MatrixHTTPMethod::Put => self.reqwest_client.put(endpoint), + MatrixHTTPMethod::Delete => self.reqwest_client.delete(endpoint), + MatrixHTTPMethod::Post => self.reqwest_client.post(endpoint), + }; + + if !headers.contains_key(&USER_AGENT) { + let user_agent = &format!("libmatrix-client/{}", env!("CARGO_PKG_VERSION")); + headers.insert(USER_AGENT, HeaderValue::from_str(user_agent)?); + } + + if !headers.contains_key(&CONTENT_TYPE) { + headers.insert(CONTENT_TYPE, HeaderValue::from_str("application/json")?); + } + + if let Some(token) = &self.access_token { + if self.use_auth_header { + request = request.bearer_auth(token); + } else { + query_params.insert("access_token".to_string(), token.to_string()); + } + } + + if let Some(id) = &self.mxid { + query_params.insert("user_id".to_string(), id.to_string()); + } + + request = request.headers(headers).query(&query_params); + + if let Some(content) = content { + request = request.body(content); + } + + loop { + let mut res = request + .try_clone() + .expect("Unable to clone request") + .send()?; + + if res.status().is_success() { + return Ok(res.text()?); + } else if res.status() == StatusCode::TOO_MANY_REQUESTS { + let mut body: HashMap = res.json()?; + if let Some(value) = body.get("error") { + body = serde_json::from_str(value)?; + } + + if let Some(value) = body.get("retry_after_ms") { + std::thread::sleep(time::Duration::from_millis(value.parse::()?)); + } else { + std::thread::sleep(time::Duration::from_millis(self.default_492_wait_ms)); + } + } else { + return Err(Box::from(ResponseError { + code: res.status(), + content: res.text()?, + })); + } + } + } + + fn send_query( + &self, + method: MatrixHTTPMethod, + path: &str, + query_params: HashMap, + ) -> Result> { + self.send(method, Some(path), None, Some(query_params), None) + } + + pub fn sync( + &self, + bookmark_token: Option<&str>, + timeout_ms: Option, + filter: Option<&str>, + get_full_state: Option, + set_presence: Option, + ) -> Result> { + let mut params: HashMap = HashMap::with_capacity(5); + params.insert( + "timeout".to_string(), + timeout_ms.unwrap_or_else(|| 30000).to_string(), + ); + + if let Some(token) = bookmark_token { + params.insert("since".to_string(), token.to_string()); + } + + if let Some(filter) = filter { + params.insert("filter".to_string(), filter.to_string()); + } + + if let Some(true) = get_full_state { + params.insert("full_state".to_string(), "true".to_string()); + } + + params.insert( + "full_state".to_string(), + match set_presence { + Some(PresenceState::Online) => "online", + Some(PresenceState::Unavailable) => "unavailable", + None | Some(PresenceState::Offline) => "offline", + } + .to_string(), + ); + + Ok(serde_json::from_str(&self.send_query( + MatrixHTTPMethod::Get, + "/sync", + params, + )?)?) + } +} + +#[derive(Default)] +pub struct ApiError {} + +impl fmt::Display for ApiError { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "an error occurred!") + } +} diff --git a/src/api/error.rs b/src/api/error.rs new file mode 100644 index 0000000..b4f2385 --- /dev/null +++ b/src/api/error.rs @@ -0,0 +1,86 @@ +use serde::Deserialize; +use std::collections::HashMap; + +// Various helper constants for common error codes. +pub const FORBIDDEN: &str = "M_FORBIDDEN"; +pub const UNKNOWN_TOKEN: &str = "M_UNKNOWN_TOKEN"; +pub const MISSING_TOKEN: &str = "M_MISSING_TOKEN"; +pub const BAD_JSON: &str = "M_BAD_JSON"; +pub const NOT_JSON: &str = "M_NOT_JSON"; +pub const NOT_FOUND: &str = "M_NOT_FOUND"; +pub const LIMIT_EXCEEDED: &str = "M_LIMIT_EXCEEDED"; +pub const UNKNOWN: &str = "M_UNKNOWN"; +pub const UNRECOGNIZED: &str = "M_UNRECOGNIZED"; +pub const UNAUTHORIZED: &str = "M_UNAUTHORIZED"; +pub const USER_IN_USE: &str = "M_USER_IN_USE"; +pub const INVALID_USERNAME: &str = "M_INVALID_USERNAME"; +pub const IN_USE: &str = "M_ROOM_IN_USE"; +pub const STATE: &str = "M_INVALID_ROOM_STATE"; +pub const THREEPID_IN_USE: &str = "M_THREEPID_IN_USE"; +pub const THREEPID_NOT_FOUND: &str = "M_THREEPID_NOT_FOUND"; +pub const THREEPID_AUTH_FAILED: &str = "M_THREEPID_AUTH_FAILED"; +pub const THREEPID_DENIED: &str = "M_THREEPID_DENIED"; +pub const SERVER_NOT_TRUSTED: &str = "M_SERVER_NOT_TRUSTED"; +pub const UNSUPPORTED_ROOM_VERSION: &str = "M_UNSUPPORTED_ROOM_VERSION"; +pub const INCOMPATIBLE_ROOM_VERSION: &str = "M_INCOMPATIBLE_ROOM_VERSION"; +pub const BAD_STATE: &str = "M_BAD_STATE"; +pub const GUEST_ACCESS_FORBIDDEN: &str = "M_GUEST_ACCESS_FORBIDDEN"; +pub const CAPTCHA_NEEDED: &str = "M_CAPTCHA_NEEDED"; +pub const CAPTCHA_INVALID: &str = "M_CAPTCHA_INVALID"; +pub const MISSING_PARAM: &str = "M_MISSING_PARAM"; +pub const INVALID_PARAM: &str = "M_INVALID_PARAM"; +pub const TOO_LARGE: &str = "M_TOO_LARGE"; +pub const EXCLUSIVE: &str = "M_EXCLUSIVE"; +pub const RESOURCE_LIMIT_EXCEEDED: &str = "M_RESOURCE_LIMIT_EXCEEDED"; +pub const CANNOT_LEAVE_SERVER_NOTICE_ROOM: &str = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM"; + +/// Implements the standard error response as defined by [Section 2](https://matrix.org/docs/spec/client_server/r0.5.0#api-standards) +/// of the Client-Server API standard as a HashMap. We don't have a better way +/// of representing this class, since we need to conform to having different +/// fields for different errors. +#[derive(Deserialize)] +pub struct ApiError { + #[serde(flatten)] + pub data: HashMap, +} + +impl ApiError { + /// Helper function for obtaining the error code. Access the `errcode` + /// field, which must be present on all valid error messages. + pub fn error_code(&self) -> &str { + self.data + .get("errcode") + .expect("Error did not contain required errcode field") + } + + /// Helper function for obtaining the error message, which is a + /// human-readable field that explains the issue. This accesses the `error` + /// field, which must be present on all valid error messages. + pub fn error_message(&self) -> &str { + self.data + .get("error") + .expect("Error did not contain required error field") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserialize_basic_error_msg() { + let test_msg = r#"{"errcode": "M_FORBIDDEN", "error": "hello"}"#; + let deserialized: ApiError = serde_json::from_str(test_msg).unwrap(); + assert_eq!(deserialized.error_code(), FORBIDDEN); + assert_eq!(deserialized.error_message(), "hello"); + } + + #[test] + fn deserialize_error_with_custom_field() { + let test_msg = r#"{"errcode": "M_UNKNOWN", "error": "foo", "a": "b"}"#; + let deserialized: ApiError = serde_json::from_str(test_msg).unwrap(); + assert_eq!(deserialized.error_code(), UNKNOWN); + assert_eq!(deserialized.error_message(), "foo"); + assert_eq!(deserialized.data.get("a").unwrap(), "b"); + } +} diff --git a/src/api/methods/mod.rs b/src/api/methods/mod.rs new file mode 100644 index 0000000..d086d5b --- /dev/null +++ b/src/api/methods/mod.rs @@ -0,0 +1 @@ +pub mod sync; diff --git a/src/api/methods/sync.rs b/src/api/methods/sync.rs new file mode 100644 index 0000000..9cf98f0 --- /dev/null +++ b/src/api/methods/sync.rs @@ -0,0 +1,169 @@ +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Deserialize)] +pub struct SyncResponse { + pub next_batch: String, + pub rooms: Option, + pub presence: Option, + pub account_data: Option, + pub to_device: Option, + pub device_lists: Option, + pub device_one_time_keys_count: Option>, +} + +#[derive(Deserialize)] +pub struct Rooms { + pub join: Option>, + pub invite: Option>, + pub leave: Option>, +} + +#[derive(Deserialize)] +pub struct JoinedRoom { + pub summary: Option, + pub state: Option, + pub timeline: Option, + pub ephemeral: Option, + pub account_date: Option, + pub unread_notifications: Option, +} + +#[derive(Deserialize)] +pub struct RoomSummary { + #[serde(rename = "m.heroes")] + pub heros: Option>, + #[serde(rename = "m.joined_member_count")] + pub joined_member_count: Option, + #[serde(rename = "m.invited_member_count")] + pub invited_member_count: Option, +} + +#[derive(Deserialize)] +pub struct State { + pub events: Option, +} + +#[derive(Deserialize)] +pub struct StateEvents { + pub content: serde_json::value::Value, // This is an json object + pub r#type: String, + pub event_id: String, + pub sender: String, + pub origin_server_ts: u64, + pub unsigned: Option, + pub prev_content: Option, + pub state_key: String, +} + +#[derive(Deserialize)] +pub struct UnsignedData { + pub age: Option, + pub redacted_because: Option, + pub transaction_id: Option, +} + +#[derive(Deserialize)] +pub struct Event { + pub content: serde_json::value::Value, // Json object + pub r#type: String, +} + +#[derive(Deserialize)] +pub struct EventContent { + pub avatar_url: Option, + pub displayname: Option, //type is string or null???? + pub membership: Membership, + pub state_key: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Membership { + Invite, + Join, + Knock, + Leave, + Ban, +} + +#[derive(Deserialize)] +pub struct Timeline { + pub events: Option>, + pub limited: Option, + pub prev_batch: Option, +} + +#[derive(Deserialize)] +pub struct RoomEvent { + pub content: String, + pub r#type: String, + pub event_id: String, + pub sender: String, + pub origin_server_ts: u64, + pub unsigned: Option, +} + +#[derive(Deserialize)] +pub struct Ephemeral { + pub events: Option>, +} + +#[derive(Deserialize)] +pub struct AccountData { + pub events: Option>, +} + +#[derive(Deserialize)] +pub struct UnreadNotificationCounts { + pub highlight_count: Option, + pub notification_count: Option, +} + +#[derive(Deserialize)] +pub struct InvitedRoom { + pub invite_state: Option, +} + +#[derive(Deserialize)] +pub struct InviteState { + pub events: Option>, +} + +#[derive(Deserialize)] +pub struct StrippedState { + pub content: EventContent, + pub state_key: String, + pub r#type: String, + pub sender: String, +} + +#[derive(Deserialize)] +pub struct LeftRoom { + pub state: Option, + pub timeline: Option, + pub account_data: Option, +} + +#[derive(Deserialize)] +pub struct Presence { + pub events: Option>, +} + +#[derive(Deserialize)] +pub struct ToDevice { + pub events: Option>, +} + +#[derive(Deserialize)] +pub struct ToDeviceEvent { + pub content: Option, + pub sender: Option, + pub r#type: Option, +} + +#[derive(Deserialize)] +pub struct DeviceLists { + pub changed: Option>, + pub left: Option>, +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 5a9b2cf..1d11e22 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,19 +1,14 @@ -use std::fmt; +use serde::Deserialize; +use std::collections::HashMap; -#[derive(Debug, PartialEq)] -pub struct Client {} +pub mod client; +pub mod error; +pub mod methods; -impl Client{ - pub fn new() -> Self { - Client {} - } -} - -#[derive(Default)] -pub struct ApiError {} - -impl fmt::Display for ApiError { - fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "an error occurred!") - } +pub type Result = std::result::Result; + +#[derive(Deserialize)] +pub struct VersionApiResp { + pub versions: Vec, + pub unstable_features: HashMap, } diff --git a/src/api/rooms.rs b/src/api/rooms.rs new file mode 100644 index 0000000..be1dc00 --- /dev/null +++ b/src/api/rooms.rs @@ -0,0 +1,3 @@ +trait RoomApi { + fn send_state_event(room_id: &str, ) +} diff --git a/src/api/util.rs b/src/api/util.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/lib.rs b/src/lib.rs index 579256c..5b05d20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ #![forbid(unsafe_code)] +// #![warn(missing_docs)] -mod api; +pub mod api; mod client; mod room; mod user; diff --git a/src/room/events.rs b/src/room/events.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/user.rs b/src/user.rs index 8953b1d..6f793a1 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,4 +1,4 @@ -use crate::api::{ApiError, Client}; +use crate::api::client::{ApiError, Client}; use crate::room::Room; use std::fmt; @@ -29,13 +29,19 @@ impl fmt::Display for UserInitError { } } -#[derive(Debug, PartialEq)] +#[derive(Debug)] struct User { id: String, display_name: Option, client: Client, } +impl PartialEq for User { + fn eq(&self, rhs: &Self) -> bool { + self.id == rhs.id && self.display_name == rhs.display_name + } +} + impl User { /// Constructs a new User. This represents a user in a room. /// @@ -46,7 +52,11 @@ impl User { /// /// Returns either a new User struct or an error containing the reason why it failed to create a /// new User. - pub fn new(id: String, display_name: Option) -> Result { + pub fn new( + client: Client, + id: String, + display_name: Option, + ) -> Result { if !id.starts_with("@") { Err(UserInitError::new( "User ID must start with a @", @@ -61,7 +71,7 @@ impl User { Ok(User { id, display_name, - client: Client::new(), + client, }) } } @@ -90,7 +100,11 @@ mod tests { #[test] fn new_returns_err_on_invalid_id() { assert_eq!( - User::new(String::from("abc:edf"), None), + User::new( + Client::new("https://google.com", None, None, None, None).unwrap(), + String::from("abc:edf"), + None + ), Err(UserInitError { message: "User ID must start with a @".to_string(), reason: UserInitErrorReason::InvalidUsername @@ -98,7 +112,11 @@ mod tests { ); assert_eq!( - User::new(String::from("@abcedf"), None), + User::new( + Client::new("https://google.com", None, None, None, None).unwrap(), + String::from("@abcedf"), + None + ), Err(UserInitError { message: "User ID must contain a :".to_string(), reason: UserInitErrorReason::NoDomainProvided @@ -109,11 +127,15 @@ mod tests { #[test] fn new_returns_struct_on_valid_input() { assert_eq!( - User::new("@eddie:eddie.sh".to_string(), None), + User::new( + Client::new("https://google.com", None, None, None, None).unwrap(), + "@eddie:eddie.sh".to_string(), + None + ), Ok(User { id: "@eddie:eddie.sh".to_string(), display_name: None, - client: Client::new() + client: Client::new("https://google.com", None, None, None, None).unwrap() }) ) }