community: the drawer asks a peer for its card and the server for interests, and discover gets the similar-users shelf

This commit is contained in:
KeelrLeek committed 2026-09-02 14:03:26 -05:00
1 parent 64a7561750
commit 5160d97a5f
10 files changed
+412 -2

No files matched your search

+32
View File
@@ -277,6 +277,23 @@ fn handle_server_frame(inner: &Arc<Inner>, mut body: Bytes) {
dir_count,
});
}
server::ServerEvent::UserInterests { username, likes, hates } => {
let waiters = inner
.pending_interests
.lock()
.unwrap()
.remove(&username)
.unwrap_or_default();
for tx in waiters {
let _ = tx.send((likes.clone(), hates.clone()));
}
}
server::ServerEvent::SimilarUsers { users } => {
let waiters: Vec<_> = inner.pending_similar.lock().unwrap().drain(..).collect();
for tx in waiters {
let _ = tx.send(users.clone());
}
}
server::ServerEvent::CannotConnect { .. } | server::ServerEvent::Unhandled { .. } => {}
}
}
@@ -608,6 +625,21 @@ fn route_peer_event(inner: &Arc<Inner>, username: &str, ev: peer::PeerEvent) {
}
}
}
UserInfoResponse { description, picture, total_uploads, queue_size, has_free_slot } => {
let info = crate::UserInfo { description, picture, total_uploads, queue_size, has_free_slot };
let waiters = inner
.pending_user_infos
.lock()
.unwrap()
.remove(username)
.unwrap_or_default();
if waiters.is_empty() {
log::debug!("unsolicited user info from {username}");
}
for tx in waiters {
let _ = tx.send(info.clone());
}
}
UserInfoRequest => {
if let Some(h) = inner.peers.lock().unwrap().get(username) {
crate::responder::respond_user_info(inner, h);
+92
View File
@@ -184,6 +184,17 @@ pub(crate) struct TransferAck {
/// or the denial that arrived instead.
pub(crate) type UploadOffer = std::result::Result<(u32, u64), String>; // (remote token, size)
/// A peer's user-info card as they sent it. The picture is untouched bytes:
/// the app decides whether and how to decode it.
#[derive(Debug, Clone)]
pub struct UserInfo {
pub description: String,
pub picture: Option<Vec<u8>>,
pub total_uploads: u32,
pub queue_size: u32,
pub has_free_slot: bool,
}
pub(crate) struct Inner {
pub server_tx: mpsc::UnboundedSender<Vec<u8>>,
pub event_tx: mpsc::UnboundedSender<Event>,
@@ -206,6 +217,9 @@ pub(crate) struct Inner {
pub peers: Mutex<HashMap<String, PeerHandle>>,
/// Browse requests awaiting a peer's share listing, by username.
pub pending_browses: Mutex<HashMap<String, Vec<oneshot::Sender<Vec<Directory>>>>>,
pub pending_user_infos: Mutex<HashMap<String, Vec<oneshot::Sender<UserInfo>>>>,
pub pending_interests: Mutex<HashMap<String, Vec<oneshot::Sender<(Vec<String>, Vec<String>)>>>>,
pub pending_similar: Mutex<Vec<oneshot::Sender<Vec<(String, u32)>>>>,
/// TransferResponse waits: (username, our token).
pub transfer_waits: Mutex<HashMap<(String, u32), oneshot::Sender<TransferAck>>>,
/// TransferRequest(upload) waits: (username, remote filename).
@@ -317,6 +331,9 @@ impl Client {
expected_transfers: Mutex::new(HashMap::new()),
peers: Mutex::new(HashMap::new()),
pending_browses: Mutex::new(HashMap::new()),
pending_user_infos: Mutex::new(HashMap::new()),
pending_interests: Mutex::new(HashMap::new()),
pending_similar: Mutex::new(Vec::new()),
transfer_waits: Mutex::new(HashMap::new()),
upload_offers: Mutex::new(HashMap::new()),
downloads_by_file: Mutex::new(HashMap::new()),
@@ -549,6 +566,81 @@ impl Client {
}
}
/// A user's info card over a peer connection — spawn-shaped like
/// `browse`: drop whatever lock guarded the `Client` before awaiting.
pub fn user_info(
&self,
username: &str,
) -> impl std::future::Future<Output = Result<UserInfo>> + Send + 'static {
let inner = self.inner.clone();
let username = username.to_string();
async move {
let handle = conn::get_or_connect_peer(&inner, &username).await?;
let (tx, rx) = oneshot::channel();
inner
.pending_user_infos
.lock()
.unwrap()
.entry(username.clone())
.or_default()
.push(tx);
handle
.write_tx
.send(peer::user_info_request())
.map_err(|_| ClientError::PeerUnreachable(username.clone()))?;
match tokio::time::timeout(std::time::Duration::from_secs(30), rx).await {
Ok(Ok(info)) => Ok(info),
_ => {
inner.pending_user_infos.lock().unwrap().remove(&username);
Err(ClientError::Timeout)
}
}
}
}
/// A user's declared likes and hates, from the server. Spawn-shaped.
pub fn user_interests(
&self,
username: &str,
) -> impl std::future::Future<Output = Result<(Vec<String>, Vec<String>)>> + Send + 'static {
let inner = self.inner.clone();
let username = username.to_string();
async move {
let (tx, rx) = oneshot::channel();
inner
.pending_interests
.lock()
.unwrap()
.entry(username.clone())
.or_default()
.push(tx);
inner.send_server(server::user_interests(&username))?;
match tokio::time::timeout(std::time::Duration::from_secs(20), rx).await {
Ok(Ok(v)) => Ok(v),
_ => {
inner.pending_interests.lock().unwrap().remove(&username);
Err(ClientError::Timeout)
}
}
}
}
/// Users the server ranks as similar to us by shared interests. Spawn-shaped.
pub fn similar_users(
&self,
) -> impl std::future::Future<Output = Result<Vec<(String, u32)>>> + Send + 'static {
let inner = self.inner.clone();
async move {
let (tx, rx) = oneshot::channel();
inner.pending_similar.lock().unwrap().push(tx);
inner.send_server(server::similar_users())?;
match tokio::time::timeout(std::time::Duration::from_secs(20), rx).await {
Ok(Ok(v)) => Ok(v),
_ => Err(ClientError::Timeout),
}
}
}
pub fn listen_port(&self) -> u16 {
self.inner.listen_port
}