feat(lib): answer UserInfoRequest with our slots and queue

Opening a user's profile in SoulseekQt or Nicotine+ asks that peer
directly (peer code 15), and a peer that never answers shows an endless
spinner. The reply (code 16) carries no description or picture, the
configured upload slot count, how many uploads are waiting, and whether a
slot is free right now, which is what the reference clients send.
This commit is contained in:
Michel de Graaf
2026-09-07 15:32:30 +02:00
parent 7c4d9e2c9a
commit 73208ae927
6 changed files with 125 additions and 36 deletions
+24 -36
View File
@@ -6,6 +6,7 @@ use crate::message::peer::{
PlaceInQueueRequest, PlaceInQueueResponse, QueueUploadHandler,
SharedDirectory, SharedFileListResponseHandler, TransferRequest,
TransferResponse, UploadDeniedHandler, UploadFailedHandler,
UserInfoRequest,
};
use crate::message::server::MessageFactory;
use crate::message::{Handlers, Message, MessageReader, MessageType};
@@ -46,6 +47,8 @@ pub enum PeerMessage {
ShareListRequested,
/// A peer we are browsing sent us their shared-file listing (code 5).
ShareListReceived(Vec<SharedDirectory>),
/// A peer asked what we say about ourselves (they sent us code 15).
UserInfoRequested,
/// A peer asked for one folder of our shares (they sent us code 36).
FolderContentsRequested {
token: u32,
@@ -184,6 +187,7 @@ impl PeerActor {
handlers.register_handler(TransferResponse);
handlers.register_handler(GetShareFileList);
handlers.register_handler(FolderContentsRequest);
handlers.register_handler(UserInfoRequest);
handlers.register_handler(UploadDeniedHandler);
handlers.register_handler(UploadFailedHandler);
handlers.register_handler(PlaceInQueueRequest);
@@ -274,13 +278,27 @@ impl PeerActor {
self.handle_serve_upload(token, filename, size);
}
PeerMessage::ShareListRequested => {
self.handle_share_list_requested();
self.forward(ClientOperation::ShareListRequested {
requester_key: self.peer_username(),
});
}
PeerMessage::ShareListReceived(directories) => {
self.handle_share_list_received(directories);
self.forward(ClientOperation::BrowseResult {
username: self.peer_username(),
directories,
});
}
PeerMessage::UserInfoRequested => {
self.forward(ClientOperation::UserInfoRequested {
requester_key: self.peer_username(),
});
}
PeerMessage::FolderContentsRequested { token, folder } => {
self.handle_folder_contents_requested(token, folder);
self.forward(ClientOperation::FolderContentsRequested {
requester_key: self.peer_username(),
token,
folder,
});
}
PeerMessage::RequestTransfer(download) => {
let message = MessageFactory::build_transfer_request_message(
@@ -440,39 +458,9 @@ impl PeerActor {
self.send_message(message);
}
fn handle_share_list_requested(&self) {
let requester_key = self.peer_username();
if let Err(e) = self
.client_channel
.send(ClientOperation::ShareListRequested { requester_key })
{
error!("[peer_actor] forward ShareListRequested: {}", e);
}
}
fn handle_folder_contents_requested(&self, token: u32, folder: String) {
let requester_key = self.peer_username();
if let Err(e) =
self.client_channel
.send(ClientOperation::FolderContentsRequested {
requester_key,
token,
folder,
})
{
error!("[peer_actor] forward FolderContentsRequested: {}", e);
}
}
fn handle_share_list_received(&self, directories: Vec<SharedDirectory>) {
let username = self.peer_username();
if let Err(e) =
self.client_channel.send(ClientOperation::BrowseResult {
username,
directories,
})
{
error!("[peer_actor] forward BrowseResult: {}", e);
fn forward(&self, operation: ClientOperation) {
if let Err(e) = self.client_channel.send(operation) {
error!("[peer_actor] forward to client: {}", e);
}
}
+4
View File
@@ -292,6 +292,10 @@ pub enum ClientOperation {
ShareListRequested {
requester_key: String,
},
/// A peer asked what we say about ourselves; send a UserInfoResponse.
UserInfoRequested {
requester_key: String,
},
/// A peer asked for one folder of our shares; send a FolderContentsResponse.
FolderContentsRequested {
requester_key: String,
+13
View File
@@ -766,6 +766,19 @@ impl Client {
},
);
}
ClientOperation::UserInfoRequested { requester_key } => {
Self::reply_to_peer(
&client_context,
&requester_key,
|ctx| {
crate::message::peer::build_user_info(
ctx.upload_slots as u32,
ctx.upload_queue.len() as u32,
ctx.has_free_upload_slot(),
)
},
);
}
ClientOperation::FolderContentsRequested {
requester_key,
token,
+2
View File
@@ -10,6 +10,7 @@ mod transfer_request;
mod transfer_response;
mod upload_denied;
mod upload_failed;
mod user_info;
// Re-export handlers
pub use file_search_response::{
@@ -31,3 +32,4 @@ pub use transfer_request::TransferRequest;
pub use transfer_response::TransferResponse;
pub use upload_denied::UploadDeniedHandler;
pub use upload_failed::UploadFailedHandler;
pub use user_info::{UserInfoRequest, build_user_info};
@@ -0,0 +1,33 @@
//! `UserInfoRequest` (peer code 15) and its reply (code 16): what a peer says
//! about itself when another user opens its profile.
use crate::message::{Message, MessageHandler};
use crate::peer::PeerMessage;
use std::sync::mpsc::Sender;
pub struct UserInfoRequest;
impl MessageHandler<PeerMessage> for UserInfoRequest {
fn get_code(&self) -> u32 {
15
}
fn handle(&self, _message: &mut Message, sender: Sender<PeerMessage>) {
let _ = sender.send(PeerMessage::UserInfoRequested);
}
}
/// Build a `UserInfoResponse` (peer code 16) with no description or picture.
#[must_use]
pub fn build_user_info(
upload_slots: u32,
queue_size: u32,
slots_free: bool,
) -> Message {
Message::new()
.write_int32(16)
.write_string("")
.write_bool(false)
.write_int32(upload_slots)
.write_int32(queue_size)
.write_bool(slots_free)
.clone()
}