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:
1 parent
64a7561750
commit
5160d97a5f
10 files changed
+412
-2
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -125,6 +125,11 @@ pub fn browse_request() -> Vec<u8> {
|
||||
Writer::new().finish_u32(code::BROWSE_REQUEST)
|
||||
}
|
||||
|
||||
/// Ask a peer for their user-info card (code 15; answered with code 16).
|
||||
pub fn user_info_request() -> Vec<u8> {
|
||||
Writer::new().finish_u32(code::USER_INFO_REQUEST)
|
||||
}
|
||||
|
||||
/// Legacy enqueue message; some clients expect it before TransferRequest.
|
||||
pub fn queue_download(filename: &str) -> Vec<u8> {
|
||||
Writer::new()
|
||||
@@ -247,6 +252,15 @@ pub enum PeerEvent {
|
||||
UploadFailed {
|
||||
filename: String,
|
||||
},
|
||||
/// A peer's user-info card, the answer to our user_info_request.
|
||||
UserInfoResponse {
|
||||
description: String,
|
||||
/// Raw bytes as sent — hostile input until decoded under limits.
|
||||
picture: Option<Vec<u8>>,
|
||||
total_uploads: u32,
|
||||
queue_size: u32,
|
||||
has_free_slot: bool,
|
||||
},
|
||||
// Requests other peers make of us (M5 responder):
|
||||
BrowseRequest,
|
||||
UserInfoRequest,
|
||||
@@ -320,6 +334,17 @@ pub fn decode(code: u32, payload: Bytes) -> Result<PeerEvent> {
|
||||
code::UPLOAD_FAILED => PeerEvent::UploadFailed {
|
||||
filename: r.string()?,
|
||||
},
|
||||
code::USER_INFO_RESPONSE => {
|
||||
let description = r.string()?;
|
||||
let picture = if r.bool()? { Some(r.bytes()?.to_vec()) } else { None };
|
||||
PeerEvent::UserInfoResponse {
|
||||
description,
|
||||
picture,
|
||||
total_uploads: r.u32()?,
|
||||
queue_size: r.u32()?,
|
||||
has_free_slot: r.bool().unwrap_or(false),
|
||||
}
|
||||
}
|
||||
code::BROWSE_REQUEST => PeerEvent::BrowseRequest,
|
||||
code::USER_INFO_REQUEST => PeerEvent::UserInfoRequest,
|
||||
code::FOLDER_CONTENTS_REQUEST => PeerEvent::FolderContentsRequest {
|
||||
|
||||
@@ -31,6 +31,8 @@ pub mod code {
|
||||
pub const PING: u32 = 32;
|
||||
pub const SHARED_FOLDERS_AND_FILES: u32 = 35;
|
||||
pub const GET_USER_STATS: u32 = 36;
|
||||
pub const USER_INTERESTS: u32 = 57;
|
||||
pub const SIMILAR_USERS: u32 = 110;
|
||||
pub const ROOM_LIST: u32 = 64;
|
||||
pub const HAVE_NO_PARENTS: u32 = 71;
|
||||
pub const PARENT_MIN_SPEED: u32 = 83;
|
||||
@@ -136,6 +138,27 @@ pub fn get_user_stats(username: &str) -> Vec<u8> {
|
||||
.finish_u32(code::GET_USER_STATS)
|
||||
}
|
||||
|
||||
/// A user's declared likes and hates (code 57).
|
||||
pub fn user_interests(username: &str) -> Vec<u8> {
|
||||
Writer::new()
|
||||
.string(username)
|
||||
.finish_u32(code::USER_INTERESTS)
|
||||
}
|
||||
|
||||
/// Users the server ranks as similar to us by shared interests (code 110).
|
||||
pub fn similar_users() -> Vec<u8> {
|
||||
Writer::new().finish_u32(code::SIMILAR_USERS)
|
||||
}
|
||||
|
||||
fn string_list(r: &mut Reader) -> Result<Vec<String>> {
|
||||
let n = r.u32()?.min(1024) as usize;
|
||||
let mut v = Vec::with_capacity(n.min(64));
|
||||
for _ in 0..n {
|
||||
v.push(r.string()?);
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
pub fn say_chatroom(room: &str, message: &str) -> Vec<u8> {
|
||||
Writer::new()
|
||||
.string(room)
|
||||
@@ -226,6 +249,16 @@ pub enum ServerEvent {
|
||||
Embedded(Bytes),
|
||||
/// Public rooms and their user counts (pushed after login, and on
|
||||
/// request). Private-room sections in the payload are ignored.
|
||||
/// Answer to user_interests: what they declared, verbatim.
|
||||
UserInterests {
|
||||
username: String,
|
||||
likes: Vec<String>,
|
||||
hates: Vec<String>,
|
||||
},
|
||||
/// Answer to similar_users: (username, rating) as the server ranks them.
|
||||
SimilarUsers {
|
||||
users: Vec<(String, u32)>,
|
||||
},
|
||||
RoomList {
|
||||
rooms: Vec<(String, u32)>,
|
||||
},
|
||||
@@ -473,6 +506,20 @@ pub fn decode(code: u32, payload: Bytes) -> Result<ServerEvent> {
|
||||
dir_count: r.u32()?,
|
||||
}
|
||||
}
|
||||
code::USER_INTERESTS => {
|
||||
let username = r.string()?;
|
||||
let likes = string_list(&mut r)?;
|
||||
let hates = string_list(&mut r)?;
|
||||
ServerEvent::UserInterests { username, likes, hates }
|
||||
}
|
||||
code::SIMILAR_USERS => {
|
||||
let n = r.u32()?.min(4096) as usize;
|
||||
let mut users = Vec::with_capacity(n.min(256));
|
||||
for _ in 0..n {
|
||||
users.push((r.string()?, r.u32()?));
|
||||
}
|
||||
ServerEvent::SimilarUsers { users }
|
||||
}
|
||||
code::ROOM_LIST => {
|
||||
// Public names + counts; the private-room sections that follow
|
||||
// are deliberately not parsed.
|
||||
|
||||
@@ -160,6 +160,16 @@ impl Reader {
|
||||
})
|
||||
}
|
||||
|
||||
/// A length-prefixed raw byte string (a user picture, for one): the same
|
||||
/// framing as `string`, without the UTF-8 interpretation.
|
||||
pub fn bytes(&mut self) -> Result<Bytes> {
|
||||
let len = self.u32()? as usize;
|
||||
if self.buf.remaining() < len {
|
||||
return Err(ProtoError::UnexpectedEof);
|
||||
}
|
||||
Ok(self.buf.copy_to_bytes(len))
|
||||
}
|
||||
|
||||
/// Replace the remaining payload with its zlib-decompressed form.
|
||||
pub fn decompress(&mut self) -> Result<()> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
Reference in new issue
Block a user