diff --git a/services/commtest/tests/identity_device_list_tests.rs b/services/commtest/tests/identity_device_list_tests.rs --- a/services/commtest/tests/identity_device_list_tests.rs +++ b/services/commtest/tests/identity_device_list_tests.rs @@ -1,3 +1,4 @@ +use std::collections::{HashMap, HashSet}; use std::str::FromStr; use commtest::identity::device::{ @@ -7,7 +8,9 @@ use commtest::service_addr; use grpc_clients::identity::authenticated::ChainedInterceptedAuthClient; use grpc_clients::identity::get_auth_client; -use grpc_clients::identity::protos::auth::UpdateDeviceListRequest; +use grpc_clients::identity::protos::auth::{ + PeersDeviceListsRequest, PeersDeviceListsResponse, UpdateDeviceListRequest, +}; use grpc_clients::identity::protos::authenticated::GetDeviceListRequest; use grpc_clients::identity::DeviceType; use serde::Deserialize; @@ -223,6 +226,70 @@ assert_eq!(device_lists_response, expected_device_list); } +#[tokio::test] +async fn test_device_list_multifetch() { + // Create viewer (user that only auths request) + let viewer = register_user_device(None, None).await; + let mut auth_client = get_auth_client( + &service_addr::IDENTITY_GRPC.to_string(), + viewer.user_id.clone(), + viewer.device_id, + viewer.access_token, + PLACEHOLDER_CODE_VERSION, + DEVICE_TYPE.to_string(), + ) + .await + .expect("Couldn't connect to identity service"); + + // Register users and prepare expected device lists + let mut users = Vec::new(); + for _ in 0..5 { + let user = register_user_device(None, None).await; + users.push(user); + } + let user_ids: Vec<_> = + users.iter().map(|user| user.user_id.clone()).collect(); + let expected_device_lists: HashMap> = users + .into_iter() + .map(|user| (user.user_id, vec![user.device_id])) + .collect(); + + // Fetch device lists from server + let request = PeersDeviceListsRequest { user_ids }; + let response_device_lists = auth_client + .get_device_lists_for_users(request) + .await + .expect("GetDeviceListsForUser RPC failed") + .into_inner() + .users_device_lists; + + // verify if response has the same user IDs as request + let expected_user_ids: HashSet = + expected_device_lists.keys().cloned().collect(); + let response_user_ids: HashSet = + response_device_lists.keys().cloned().collect(); + let difference: HashSet<_> = expected_user_ids + .symmetric_difference(&response_user_ids) + .collect(); + assert!(difference.is_empty(), "User IDs differ: {:?}", difference); + + // verify device list for each user + for (user_id, expected_devices) in expected_device_lists { + let response_payload = response_device_lists.get(&user_id).unwrap(); + + let returned_devices = SignedDeviceList::from_str(response_payload) + .expect("failed to deserialize signed device list") + .into_raw() + .devices; + + assert_eq!( + returned_devices, expected_devices, + "Device list differs for user: {}, Expected {:?}, but got {:?}", + user_id, expected_devices, returned_devices + ); + } +} + // See GetDeviceListResponse in identity_authenticated.proto // for details on the response format. #[derive(Deserialize)] diff --git a/services/identity/src/grpc_services/authenticated.rs b/services/identity/src/grpc_services/authenticated.rs --- a/services/identity/src/grpc_services/authenticated.rs +++ b/services/identity/src/grpc_services/authenticated.rs @@ -24,7 +24,10 @@ UpdateUserPasswordStartRequest, UpdateUserPasswordStartResponse, UploadOneTimeKeysRequest, }; -use super::protos::auth::{UserIdentityRequest, UserIdentityResponse}; +use super::protos::auth::{ + PeersDeviceListsRequest, PeersDeviceListsResponse, UserIdentityRequest, + UserIdentityResponse, +}; use super::protos::unauth::Empty; #[derive(derive_more::Constructor)] @@ -364,18 +367,62 @@ let stringified_updates = device_list_updates .iter() - .map(serde_json::to_string) - .collect::, _>>() - .map_err(|err| { - error!("Failed to serialize device list updates: {}", err); - tonic::Status::failed_precondition("unexpected error") - })?; + .map(SignedDeviceList::as_json_string) + .collect::, _>>()?; Ok(Response::new(GetDeviceListResponse { device_list_updates: stringified_updates, })) } + async fn get_device_lists_for_users( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let PeersDeviceListsRequest { user_ids } = request.into_inner(); + + // do all fetches concurrently + let mut fetch_tasks = tokio::task::JoinSet::new(); + let mut device_lists = HashMap::with_capacity(user_ids.len()); + for user_id in user_ids { + let db_client = self.db_client.clone(); + fetch_tasks.spawn(async move { + let result = db_client.get_current_device_list(&user_id).await; + (user_id, result) + }); + } + + while let Some(task_result) = fetch_tasks.join_next().await { + match task_result { + Ok((user_id, Ok(Some(device_list_row)))) => { + let raw_list = RawDeviceList::from(device_list_row); + let signed_list = SignedDeviceList::try_from_raw(raw_list)?; + let serialized_list = signed_list.as_json_string()?; + device_lists.insert(user_id, serialized_list); + } + Ok((user_id, Ok(None))) => { + warn!(user_id, "User has no device list, skipping!"); + } + Ok((user_id, Err(err))) => { + error!(user_id, "Failed fetching device list: {err}"); + // abort fetching other users + fetch_tasks.abort_all(); + return Err(handle_db_error(err)); + } + Err(join_error) => { + error!("Failed to join device list task: {join_error}"); + fetch_tasks.abort_all(); + return Err(Status::aborted("unexpected error")); + } + } + } + + let response = PeersDeviceListsResponse { + users_device_lists: device_lists, + }; + Ok(Response::new(response)) + } + async fn update_device_list( &self, request: tonic::Request, @@ -513,6 +560,14 @@ tonic::Status::invalid_argument("invalid device list payload") }) } + + /// Serializes the signed device list to a JSON string + fn as_json_string(&self) -> Result { + serde_json::to_string(self).map_err(|err| { + error!("Failed to serialize device list updates: {}", err); + tonic::Status::failed_precondition("unexpected error") + }) + } } impl TryFrom for SignedDeviceList { diff --git a/shared/protos/identity_auth.proto b/shared/protos/identity_auth.proto --- a/shared/protos/identity_auth.proto +++ b/shared/protos/identity_auth.proto @@ -58,6 +58,9 @@ // Returns device list history rpc GetDeviceListForUser(GetDeviceListRequest) returns (GetDeviceListResponse) {} + // Returns current device list for a set of users + rpc GetDeviceListsForUsers (PeersDeviceListsRequest) returns + (PeersDeviceListsResponse) {} rpc UpdateDeviceList(UpdateDeviceListRequest) returns (identity.unauth.Empty) {} @@ -198,6 +201,19 @@ repeated string device_list_updates = 1; } +// GetDeviceListsForUsers + +message PeersDeviceListsRequest { + repeated string user_ids = 1; +} + +message PeersDeviceListsResponse { + // keys are user_id + // values are JSON-stringified device list payloads + // (see GetDeviceListResponse message for payload structure) + map users_device_lists = 1; +} + // UpdateDeviceListForUser message UpdateDeviceListRequest { diff --git a/web/protobufs/identity-auth-client.cjs b/web/protobufs/identity-auth-client.cjs --- a/web/protobufs/identity-auth-client.cjs +++ b/web/protobufs/identity-auth-client.cjs @@ -688,6 +688,67 @@ }; +/** + * @const + * @type {!grpc.web.MethodDescriptor< + * !proto.identity.auth.PeersDeviceListsRequest, + * !proto.identity.auth.PeersDeviceListsResponse>} + */ +const methodDescriptor_IdentityClientService_GetDeviceListsForUsers = new grpc.web.MethodDescriptor( + '/identity.auth.IdentityClientService/GetDeviceListsForUsers', + grpc.web.MethodType.UNARY, + proto.identity.auth.PeersDeviceListsRequest, + proto.identity.auth.PeersDeviceListsResponse, + /** + * @param {!proto.identity.auth.PeersDeviceListsRequest} request + * @return {!Uint8Array} + */ + function(request) { + return request.serializeBinary(); + }, + proto.identity.auth.PeersDeviceListsResponse.deserializeBinary +); + + +/** + * @param {!proto.identity.auth.PeersDeviceListsRequest} request The + * request proto + * @param {?Object} metadata User defined + * call metadata + * @param {function(?grpc.web.RpcError, ?proto.identity.auth.PeersDeviceListsResponse)} + * callback The callback function(error, response) + * @return {!grpc.web.ClientReadableStream|undefined} + * The XHR Node Readable Stream + */ +proto.identity.auth.IdentityClientServiceClient.prototype.getDeviceListsForUsers = + function(request, metadata, callback) { + return this.client_.rpcCall(this.hostname_ + + '/identity.auth.IdentityClientService/GetDeviceListsForUsers', + request, + metadata || {}, + methodDescriptor_IdentityClientService_GetDeviceListsForUsers, + callback); +}; + + +/** + * @param {!proto.identity.auth.PeersDeviceListsRequest} request The + * request proto + * @param {?Object=} metadata User defined + * call metadata + * @return {!Promise} + * Promise that resolves to the response + */ +proto.identity.auth.IdentityClientServicePromiseClient.prototype.getDeviceListsForUsers = + function(request, metadata) { + return this.client_.unaryCall(this.hostname_ + + '/identity.auth.IdentityClientService/GetDeviceListsForUsers', + request, + metadata || {}, + methodDescriptor_IdentityClientService_GetDeviceListsForUsers); +}; + + /** * @const * @type {!grpc.web.MethodDescriptor< diff --git a/web/protobufs/identity-auth-client.cjs.flow b/web/protobufs/identity-auth-client.cjs.flow --- a/web/protobufs/identity-auth-client.cjs.flow +++ b/web/protobufs/identity-auth-client.cjs.flow @@ -81,6 +81,13 @@ response: identityAuthStructs.GetDeviceListResponse) => void ): grpcWeb.ClientReadableStream; + getDeviceListsForUsers( + request: identityAuthStructs.PeersDeviceListsRequest, + metadata: grpcWeb.Metadata | void, + callback: (err: grpcWeb.RpcError, + response: identityAuthStructs.PeersDeviceListsResponse) => void + ): grpcWeb.ClientReadableStream; + updateDeviceList( request: identityAuthStructs.UpdateDeviceListRequest, metadata: grpcWeb.Metadata | void, @@ -165,6 +172,11 @@ metadata?: grpcWeb.Metadata ): Promise; + getDeviceListsForUsers( + request: identityAuthStructs.PeersDeviceListsRequest, + metadata?: grpcWeb.Metadata + ): Promise; + updateDeviceList( request: identityAuthStructs.UpdateDeviceListRequest, metadata?: grpcWeb.Metadata diff --git a/web/protobufs/identity-auth-structs.cjs b/web/protobufs/identity-auth-structs.cjs --- a/web/protobufs/identity-auth-structs.cjs +++ b/web/protobufs/identity-auth-structs.cjs @@ -36,6 +36,8 @@ goog.exportSymbol('proto.identity.auth.OutboundKeyInfo', null, global); goog.exportSymbol('proto.identity.auth.OutboundKeysForUserRequest', null, global); goog.exportSymbol('proto.identity.auth.OutboundKeysForUserResponse', null, global); +goog.exportSymbol('proto.identity.auth.PeersDeviceListsRequest', null, global); +goog.exportSymbol('proto.identity.auth.PeersDeviceListsResponse', null, global); goog.exportSymbol('proto.identity.auth.RefreshUserPrekeysRequest', null, global); goog.exportSymbol('proto.identity.auth.UpdateDeviceListRequest', null, global); goog.exportSymbol('proto.identity.auth.UpdateUserPasswordFinishRequest', null, global); @@ -380,6 +382,48 @@ */ proto.identity.auth.GetDeviceListResponse.displayName = 'proto.identity.auth.GetDeviceListResponse'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.identity.auth.PeersDeviceListsRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.identity.auth.PeersDeviceListsRequest.repeatedFields_, null); +}; +goog.inherits(proto.identity.auth.PeersDeviceListsRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.identity.auth.PeersDeviceListsRequest.displayName = 'proto.identity.auth.PeersDeviceListsRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.identity.auth.PeersDeviceListsResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.identity.auth.PeersDeviceListsResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.identity.auth.PeersDeviceListsResponse.displayName = 'proto.identity.auth.PeersDeviceListsResponse'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -3533,6 +3577,296 @@ +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.identity.auth.PeersDeviceListsRequest.repeatedFields_ = [1]; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.identity.auth.PeersDeviceListsRequest.prototype.toObject = function(opt_includeInstance) { + return proto.identity.auth.PeersDeviceListsRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.identity.auth.PeersDeviceListsRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.identity.auth.PeersDeviceListsRequest.toObject = function(includeInstance, msg) { + var f, obj = { + userIdsList: (f = jspb.Message.getRepeatedField(msg, 1)) == null ? undefined : f + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.identity.auth.PeersDeviceListsRequest} + */ +proto.identity.auth.PeersDeviceListsRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.identity.auth.PeersDeviceListsRequest; + return proto.identity.auth.PeersDeviceListsRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.identity.auth.PeersDeviceListsRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.identity.auth.PeersDeviceListsRequest} + */ +proto.identity.auth.PeersDeviceListsRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.addUserIds(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.identity.auth.PeersDeviceListsRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.identity.auth.PeersDeviceListsRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.identity.auth.PeersDeviceListsRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.identity.auth.PeersDeviceListsRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getUserIdsList(); + if (f.length > 0) { + writer.writeRepeatedString( + 1, + f + ); + } +}; + + +/** + * repeated string user_ids = 1; + * @return {!Array} + */ +proto.identity.auth.PeersDeviceListsRequest.prototype.getUserIdsList = function() { + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 1)); +}; + + +/** + * @param {!Array} value + * @return {!proto.identity.auth.PeersDeviceListsRequest} returns this + */ +proto.identity.auth.PeersDeviceListsRequest.prototype.setUserIdsList = function(value) { + return jspb.Message.setField(this, 1, value || []); +}; + + +/** + * @param {string} value + * @param {number=} opt_index + * @return {!proto.identity.auth.PeersDeviceListsRequest} returns this + */ +proto.identity.auth.PeersDeviceListsRequest.prototype.addUserIds = function(value, opt_index) { + return jspb.Message.addToRepeatedField(this, 1, value, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.identity.auth.PeersDeviceListsRequest} returns this + */ +proto.identity.auth.PeersDeviceListsRequest.prototype.clearUserIdsList = function() { + return this.setUserIdsList([]); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.identity.auth.PeersDeviceListsResponse.prototype.toObject = function(opt_includeInstance) { + return proto.identity.auth.PeersDeviceListsResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.identity.auth.PeersDeviceListsResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.identity.auth.PeersDeviceListsResponse.toObject = function(includeInstance, msg) { + var f, obj = { + usersDeviceListsMap: (f = msg.getUsersDeviceListsMap()) ? f.toObject(includeInstance, undefined) : [] + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.identity.auth.PeersDeviceListsResponse} + */ +proto.identity.auth.PeersDeviceListsResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.identity.auth.PeersDeviceListsResponse; + return proto.identity.auth.PeersDeviceListsResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.identity.auth.PeersDeviceListsResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.identity.auth.PeersDeviceListsResponse} + */ +proto.identity.auth.PeersDeviceListsResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = msg.getUsersDeviceListsMap(); + reader.readMessage(value, function(message, reader) { + jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readString, null, "", ""); + }); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.identity.auth.PeersDeviceListsResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.identity.auth.PeersDeviceListsResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.identity.auth.PeersDeviceListsResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.identity.auth.PeersDeviceListsResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getUsersDeviceListsMap(true); + if (f && f.getLength() > 0) { + f.serializeBinary(1, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeString); + } +}; + + +/** + * map users_device_lists = 1; + * @param {boolean=} opt_noLazyCreate Do not create the map if + * empty, instead returning `undefined` + * @return {!jspb.Map} + */ +proto.identity.auth.PeersDeviceListsResponse.prototype.getUsersDeviceListsMap = function(opt_noLazyCreate) { + return /** @type {!jspb.Map} */ ( + jspb.Message.getMapField(this, 1, opt_noLazyCreate, + null)); +}; + + +/** + * Clears values from the map. The map will be non-null. + * @return {!proto.identity.auth.PeersDeviceListsResponse} returns this + */ +proto.identity.auth.PeersDeviceListsResponse.prototype.clearUsersDeviceListsMap = function() { + this.getUsersDeviceListsMap().clear(); + return this; +}; + + + if (jspb.Message.GENERATE_TO_OBJECT) { diff --git a/web/protobufs/identity-auth-structs.cjs.flow b/web/protobufs/identity-auth-structs.cjs.flow --- a/web/protobufs/identity-auth-structs.cjs.flow +++ b/web/protobufs/identity-auth-structs.cjs.flow @@ -379,6 +379,40 @@ deviceListUpdatesList: Array, } +declare export class PeersDeviceListsRequest extends Message { + getUserIdsList(): Array; + setUserIdsList(value: Array): PeersDeviceListsRequest; + clearUserIdsList(): PeersDeviceListsRequest; + addUserIds(value: string, index?: number): PeersDeviceListsRequest; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): PeersDeviceListsRequestObject; + static toObject(includeInstance: boolean, msg: PeersDeviceListsRequest): PeersDeviceListsRequestObject; + static serializeBinaryToWriter(message: PeersDeviceListsRequest, writer: BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): PeersDeviceListsRequest; + static deserializeBinaryFromReader(message: PeersDeviceListsRequest, reader: BinaryReader): PeersDeviceListsRequest; +} + +export type PeersDeviceListsRequestObject = { + userIdsList: Array, +} + +declare export class PeersDeviceListsResponse extends Message { + getUsersDeviceListsMap(): ProtoMap; + clearUsersDeviceListsMap(): PeersDeviceListsResponse; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): PeersDeviceListsResponseObject; + static toObject(includeInstance: boolean, msg: PeersDeviceListsResponse): PeersDeviceListsResponseObject; + static serializeBinaryToWriter(message: PeersDeviceListsResponse, writer: BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): PeersDeviceListsResponse; + static deserializeBinaryFromReader(message: PeersDeviceListsResponse, reader: BinaryReader): PeersDeviceListsResponse; +} + +export type PeersDeviceListsResponseObject = { + usersDeviceListsMap: Array<[string, string]>, +} + declare export class UpdateDeviceListRequest extends Message { getNewDeviceList(): string; setNewDeviceList(value: string): UpdateDeviceListRequest;