Implement devices API (#20)
small improvements Cargo fmt Simplify insert and update methods Review feedback Remove has_device method calls Load all devices with a single db call Remove device as in logout Put all metadata on the same tree Create userdevice key fucntion Implement devices API Implement all the devices endpoints. There's a couple of pending tasks: - Integrate the "logout" logic once it lands to master (this should remove the given device from the database). - Track and store last seen timestamp and IP. Co-authored-by: timokoesters <timo@koesters.xyz> Co-authored-by: Guillem Nieto <gnieto.talo@gmail.com>
This commit is contained in:
		
							parent
							
								
									720d48bd67
								
							
						
					
					
						commit
						ed9b544ace
					
				
					 4 changed files with 190 additions and 26 deletions
				
			
		|  | @ -13,6 +13,9 @@ use ruma_client_api::{ | |||
|         alias::{create_alias, delete_alias, get_alias}, | ||||
|         capabilities::get_capabilities, | ||||
|         config::{get_global_account_data, set_global_account_data}, | ||||
|         device::{ | ||||
|             self, delete_device, delete_devices, get_device, get_devices, update_device, | ||||
|         }, | ||||
|         directory::{ | ||||
|             self, get_public_rooms, get_public_rooms_filtered, get_room_visibility, | ||||
|             set_room_visibility, | ||||
|  | @ -52,7 +55,7 @@ use ruma_events::{ | |||
|     room::{canonical_alias, guest_access, history_visibility, join_rules, member, redaction}, | ||||
|     EventJson, EventType, | ||||
| }; | ||||
| use ruma_identifiers::{RoomAliasId, RoomId, RoomVersionId, UserId}; | ||||
| use ruma_identifiers::{DeviceId, RoomAliasId, RoomId, RoomVersionId, UserId}; | ||||
| use serde_json::{json, value::RawValue}; | ||||
| 
 | ||||
| use crate::{server_server, utils, Database, MatrixResult, Ruma}; | ||||
|  | @ -173,7 +176,7 @@ pub fn register_route( | |||
| 
 | ||||
|     // Generate new device id if the user didn't specify one
 | ||||
|     let device_id = body | ||||
|         .device_id | ||||
|         .device_id.clone() | ||||
|         .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH)); | ||||
| 
 | ||||
|     // Generate new token for the device
 | ||||
|  | @ -181,7 +184,7 @@ pub fn register_route( | |||
| 
 | ||||
|     // Add device
 | ||||
|     db.users | ||||
|         .create_device(&user_id, &device_id, &token) | ||||
|         .create_device(&user_id, &device_id, &token, body.initial_device_display_name.clone()) | ||||
|         .unwrap(); | ||||
| 
 | ||||
|     // Initial data
 | ||||
|  | @ -300,6 +303,7 @@ pub fn login_route( | |||
|     let device_id = body | ||||
|         .body | ||||
|         .device_id | ||||
|         .clone() | ||||
|         .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH)); | ||||
| 
 | ||||
|     // Generate a new token for the device
 | ||||
|  | @ -307,7 +311,7 @@ pub fn login_route( | |||
| 
 | ||||
|     // Add device
 | ||||
|     db.users | ||||
|         .create_device(&user_id, &device_id, &token) | ||||
|         .create_device(&user_id, &device_id, &token, body.initial_device_display_name.clone()) | ||||
|         .unwrap(); | ||||
| 
 | ||||
|     MatrixResult(Ok(login::Response { | ||||
|  | @ -2430,6 +2434,93 @@ pub fn get_content_thumbnail_route( | |||
|     } | ||||
| } | ||||
| 
 | ||||
| #[get("/_matrix/client/r0/devices", data = "<body>")] | ||||
| pub fn get_devices_route( | ||||
|     db: State<'_, Database>, | ||||
|     body: Ruma<get_devices::Request>, | ||||
| ) -> MatrixResult<get_devices::Response> { | ||||
|     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||
| 
 | ||||
|     let devices = db | ||||
|         .users | ||||
|         .all_devices_metadata(user_id) | ||||
|         .map(|r| r.unwrap()) | ||||
|         .collect::<Vec<device::Device>>(); | ||||
| 
 | ||||
|     MatrixResult(Ok(get_devices::Response { devices })) | ||||
| } | ||||
| 
 | ||||
| #[get("/_matrix/client/r0/devices/<device_id>", data = "<body>")] | ||||
| pub fn get_device_route( | ||||
|     db: State<'_, Database>, | ||||
|     body: Ruma<get_device::Request>, | ||||
|     device_id: DeviceId, | ||||
| ) -> MatrixResult<get_device::Response> { | ||||
|     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||
|     let device = db.users.get_device_metadata(&user_id, &device_id).unwrap(); | ||||
| 
 | ||||
|     match device { | ||||
|         None => MatrixResult(Err(Error { | ||||
|             kind: ErrorKind::NotFound, | ||||
|             message: "Device not found".to_string(), | ||||
|             status_code: http::StatusCode::NOT_FOUND, | ||||
|         })), | ||||
|         Some(device) => MatrixResult(Ok(get_device::Response { device })), | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[put("/_matrix/client/r0/devices/<device_id>", data = "<body>")] | ||||
| pub fn update_device_route( | ||||
|     db: State<'_, Database>, | ||||
|     body: Ruma<update_device::Request>, | ||||
|     device_id: DeviceId, | ||||
| ) -> MatrixResult<update_device::Response> { | ||||
|     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||
|     let device = db.users.get_device_metadata(&user_id, &device_id).unwrap(); | ||||
| 
 | ||||
|     match device { | ||||
|         None => MatrixResult(Err(Error { | ||||
|             kind: ErrorKind::NotFound, | ||||
|             message: "Device not found".to_string(), | ||||
|             status_code: http::StatusCode::NOT_FOUND, | ||||
|         })), | ||||
|         Some(mut device) => { | ||||
|             device.display_name = body.display_name.clone(); | ||||
| 
 | ||||
|             db.users | ||||
|                 .update_device_metadata(&user_id, &device_id, &device) | ||||
|                 .unwrap(); | ||||
| 
 | ||||
|             MatrixResult(Ok(update_device::Response)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[delete("/_matrix/client/r0/devices/<device_id>", data = "<body>")] | ||||
| pub fn delete_device_route( | ||||
|     db: State<'_, Database>, | ||||
|     body: Ruma<delete_device::Request>, | ||||
|     device_id: DeviceId, | ||||
| ) -> MatrixResult<delete_device::Response> { | ||||
|     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||
|     db.users.remove_device(&user_id, &device_id).unwrap(); | ||||
| 
 | ||||
|     MatrixResult(Ok(delete_device::Response)) | ||||
| } | ||||
| 
 | ||||
| #[post("/_matrix/client/r0/delete_devices", data = "<body>")] | ||||
| pub fn delete_devices_route( | ||||
|     db: State<'_, Database>, | ||||
|     body: Ruma<delete_devices::Request>, | ||||
| ) -> MatrixResult<delete_devices::Response> { | ||||
|     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||
|     for device_id in &body.devices { | ||||
|         db.users.remove_device(&user_id, &device_id).unwrap() | ||||
|     } | ||||
| 
 | ||||
|     MatrixResult(Ok(delete_devices::Response)) | ||||
| } | ||||
| 
 | ||||
| #[options("/<_segments..>")] | ||||
| pub fn options_route( | ||||
|     _segments: rocket::http::uri::Segments<'_>, | ||||
|  |  | |||
|  | @ -56,10 +56,10 @@ impl Database { | |||
|             ), | ||||
|             users: users::Users { | ||||
|                 userid_password: db.open_tree("userid_password").unwrap(), | ||||
|                 userdeviceids: db.open_tree("userdeviceids").unwrap(), | ||||
|                 userid_displayname: db.open_tree("userid_displayname").unwrap(), | ||||
|                 userid_avatarurl: db.open_tree("userid_avatarurl").unwrap(), | ||||
|                 userdeviceid_token: db.open_tree("userdeviceid_token").unwrap(), | ||||
|                 userdeviceid_metadata: db.open_tree("userdeviceid_metadata").unwrap(), | ||||
|                 token_userdeviceid: db.open_tree("token_userdeviceid").unwrap(), | ||||
|                 onetimekeyid_onetimekeys: db.open_tree("onetimekeyid_onetimekeys").unwrap(), | ||||
|                 userdeviceid_devicekeys: db.open_tree("userdeviceid_devicekeys").unwrap(), | ||||
|  |  | |||
|  | @ -1,16 +1,19 @@ | |||
| use crate::{utils, Error, Result}; | ||||
| use js_int::UInt; | ||||
| use ruma_client_api::r0::keys::{AlgorithmAndDeviceId, DeviceKeys, KeyAlgorithm, OneTimeKey}; | ||||
| use ruma_client_api::r0::{ | ||||
|     device::Device, | ||||
|     keys::{AlgorithmAndDeviceId, DeviceKeys, KeyAlgorithm, OneTimeKey}, | ||||
| }; | ||||
| use ruma_events::{to_device::AnyToDeviceEvent, EventJson, EventType}; | ||||
| use ruma_identifiers::{DeviceId, UserId}; | ||||
| use std::{collections::BTreeMap, convert::TryFrom}; | ||||
| use std::{collections::BTreeMap, convert::TryFrom, time::SystemTime}; | ||||
| 
 | ||||
| pub struct Users { | ||||
|     pub(super) userid_password: sled::Tree, | ||||
|     pub(super) userid_displayname: sled::Tree, | ||||
|     pub(super) userid_avatarurl: sled::Tree, | ||||
|     pub(super) userdeviceids: sled::Tree, | ||||
|     pub(super) userdeviceid_token: sled::Tree, | ||||
|     pub(super) userdeviceid_metadata: sled::Tree, // This is also used to check if a device exists
 | ||||
|     pub(super) token_userdeviceid: sled::Tree, | ||||
| 
 | ||||
|     pub(super) onetimekeyid_onetimekeys: sled::Tree, // OneTimeKeyId = UserId + AlgorithmAndDeviceId
 | ||||
|  | @ -105,25 +108,40 @@ impl Users { | |||
|     } | ||||
| 
 | ||||
|     /// Adds a new device to a user.
 | ||||
|     pub fn create_device(&self, user_id: &UserId, device_id: &DeviceId, token: &str) -> Result<()> { | ||||
|     pub fn create_device( | ||||
|         &self, | ||||
|         user_id: &UserId, | ||||
|         device_id: &DeviceId, | ||||
|         token: &str, | ||||
|         initial_device_display_name: Option<String>, | ||||
|     ) -> Result<()> { | ||||
|         if !self.exists(user_id)? { | ||||
|             return Err(Error::BadRequest( | ||||
|                 "tried to create device for nonexistent user", | ||||
|             )); | ||||
|         } | ||||
| 
 | ||||
|         let mut key = user_id.to_string().as_bytes().to_vec(); | ||||
|         key.push(0xff); | ||||
|         key.extend_from_slice(device_id.as_bytes()); | ||||
|         let mut userdeviceid = user_id.to_string().as_bytes().to_vec(); | ||||
|         userdeviceid.push(0xff); | ||||
|         userdeviceid.extend_from_slice(device_id.as_bytes()); | ||||
| 
 | ||||
|         self.userdeviceids.insert(key, &[])?; | ||||
|         self.userdeviceid_metadata.insert( | ||||
|             userdeviceid, | ||||
|             serde_json::to_string(&Device { | ||||
|                 device_id: device_id.clone(), | ||||
|                 display_name: initial_device_display_name, | ||||
|                 last_seen_ip: None, // TODO
 | ||||
|                 last_seen_ts: Some(SystemTime::now()), | ||||
|             })? | ||||
|             .as_bytes(), | ||||
|         )?; | ||||
| 
 | ||||
|         self.set_token(user_id, device_id, token)?; | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// Removes a device from a user
 | ||||
|     /// Removes a device from a user.
 | ||||
|     pub fn remove_device(&self, user_id: &UserId, device_id: &DeviceId) -> Result<()> { | ||||
|         let mut userdeviceid = user_id.to_string().as_bytes().to_vec(); | ||||
|         userdeviceid.push(0xff); | ||||
|  | @ -147,8 +165,7 @@ impl Users { | |||
| 
 | ||||
|         // TODO: Remove onetimekeys
 | ||||
| 
 | ||||
|         // Remove the device
 | ||||
|         self.userdeviceids.remove(userdeviceid)?; | ||||
|         self.userdeviceid_metadata.remove(&userdeviceid)?; | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
|  | @ -157,7 +174,11 @@ impl Users { | |||
|     pub fn all_device_ids(&self, user_id: &UserId) -> impl Iterator<Item = Result<DeviceId>> { | ||||
|         let mut prefix = user_id.to_string().as_bytes().to_vec(); | ||||
|         prefix.push(0xff); | ||||
|         self.userdeviceids.scan_prefix(prefix).keys().map(|bytes| { | ||||
|         // All devices have metadata
 | ||||
|         self.userdeviceid_metadata | ||||
|             .scan_prefix(prefix) | ||||
|             .keys() | ||||
|             .map(|bytes| { | ||||
|                 Ok(utils::string_from_bytes( | ||||
|                     &*bytes? | ||||
|                         .rsplit(|&b| b == 0xff) | ||||
|  | @ -173,7 +194,8 @@ impl Users { | |||
|         userdeviceid.push(0xff); | ||||
|         userdeviceid.extend_from_slice(device_id.as_bytes()); | ||||
| 
 | ||||
|         if self.userdeviceids.get(&userdeviceid)?.is_none() { | ||||
|         // All devices have metadata
 | ||||
|         if self.userdeviceid_metadata.get(&userdeviceid)?.is_none() { | ||||
|             return Err(Error::BadRequest( | ||||
|                 "Tried to set token for nonexistent device", | ||||
|             )); | ||||
|  | @ -203,7 +225,8 @@ impl Users { | |||
|         key.push(0xff); | ||||
|         key.extend_from_slice(device_id.as_bytes()); | ||||
| 
 | ||||
|         if self.userdeviceids.get(&key)?.is_none() { | ||||
|         // All devices have metadata
 | ||||
|         if self.userdeviceid_metadata.get(&key)?.is_none() { | ||||
|             return Err(Error::BadRequest( | ||||
|                 "Tried to set token for nonexistent device", | ||||
|             )); | ||||
|  | @ -396,4 +419,49 @@ impl Users { | |||
| 
 | ||||
|         Ok(events) | ||||
|     } | ||||
| 
 | ||||
|     pub fn update_device_metadata( | ||||
|         &self, | ||||
|         user_id: &UserId, | ||||
|         device_id: &DeviceId, | ||||
|         device: &Device, | ||||
|     ) -> Result<()> { | ||||
|         let mut userdeviceid = user_id.to_string().as_bytes().to_vec(); | ||||
|         userdeviceid.push(0xff); | ||||
|         userdeviceid.extend_from_slice(device_id.as_bytes()); | ||||
| 
 | ||||
|         if self.userdeviceid_metadata.get(userdeviceid)?.is_none() { | ||||
|             return Err(Error::BadRequest("device does not exist")); | ||||
|         } | ||||
| 
 | ||||
|         self.userdeviceid_metadata | ||||
|             .insert(userdeviceid, serde_json::to_string(device)?.as_bytes())?; | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// Get device metadata.
 | ||||
|     pub fn get_device_metadata( | ||||
|         &self, | ||||
|         user_id: &UserId, | ||||
|         device_id: &DeviceId, | ||||
|     ) -> Result<Option<Device>> { | ||||
|         let mut userdeviceid = user_id.to_string().as_bytes().to_vec(); | ||||
|         userdeviceid.push(0xff); | ||||
|         userdeviceid.extend_from_slice(device_id.as_bytes()); | ||||
| 
 | ||||
|         self.userdeviceid_metadata | ||||
|             .get(&userdeviceid)? | ||||
|             .map_or(Ok(None), |bytes| Ok(Some(serde_json::from_slice(&bytes)?))) | ||||
|     } | ||||
| 
 | ||||
|     pub fn all_devices_metadata(&self, user_id: &UserId) -> impl Iterator<Item = Result<Device>> { | ||||
|         let mut key = user_id.to_string().as_bytes().to_vec(); | ||||
|         key.push(0xff); | ||||
| 
 | ||||
|         self.userdeviceid_metadata | ||||
|             .scan_prefix(key) | ||||
|             .values() | ||||
|             .map(|bytes| Ok(serde_json::from_slice::<Device>(&bytes?)?)) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -81,6 +81,11 @@ fn setup_rocket() -> rocket::Rocket { | |||
|                 client_server::create_content_route, | ||||
|                 client_server::get_content_route, | ||||
|                 client_server::get_content_thumbnail_route, | ||||
|                 client_server::get_devices_route, | ||||
|                 client_server::get_device_route, | ||||
|                 client_server::update_device_route, | ||||
|                 client_server::delete_device_route, | ||||
|                 client_server::delete_devices_route, | ||||
|                 client_server::options_route, | ||||
|                 server_server::well_known_server, | ||||
|                 server_server::get_server_version, | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue