Reading Nicotine+ and slskd against what was just written turned up three places where this client had the message right and the behaviour wrong. Excluded search phrases (code 160) are not a filter on the searches we send — the server already drops those. Nicotine+ applies them to the files it offers, leaving out any whose path carries an excluded phrase, and a reply with nothing left is not sent. Ours now does the same, and the local refusal of outgoing searches is gone. Serving children now follows the same rules as Nicotine+: the limit comes from the server's ParentMinSpeed and ParentSpeedRatio against the upload speed the server records for us (capped at ten, none at all below the minimum), children are refused until something feeds us the search stream and let go when it stops, a second link from a child we already carry is refused, and AcceptChildren is re-sent whenever that answer changes. Where the server has never reported our speed there is nothing to apply the formula to, so the configured cap stands, which is how slskd runs. Queued downloads now ask where they sit every five minutes, as Nicotine+ does, instead of only when a caller asks; a brokered file connection we cannot complete reports CantConnectToPeer like a control connection does; and a peer's shares can be asked for one folder at a time (peer code 36 and its reply, 37), which is what "download folder" is built on.
465 lines
16 KiB
Rust
465 lines
16 KiB
Rust
//! The social side of the protocol: room tickers (codes 113-116), the global
|
|
//! room feed (150-152), interests and the recommendation queries built on them
|
|
//! (51/52/54/56/57/110/111/112/117/118), and the odds and ends a client owes
|
|
//! the server — a keepalive ping (32), gifting privileges (123) and changing
|
|
//! our password (142).
|
|
|
|
use super::{
|
|
Client, Recommendation, Result, RoomTicker, RwLockExt, SimilarUser,
|
|
UserInterests, error,
|
|
};
|
|
use crate::message::server::MessageFactory;
|
|
|
|
impl Client {
|
|
/// Send a keepalive ping (server code 32). The server does not reply; the
|
|
/// point is that a connection with nothing to say still has traffic on it,
|
|
/// so a NAT or the server's idle reaper does not drop it.
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn ping_server(&self) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_server_ping())
|
|
}
|
|
|
|
/// Join a private room, creating it (and owning it) if the name is free.
|
|
///
|
|
/// A name already taken by someone else's private room comes back as a
|
|
/// [`crate::RoomEvent::CantCreate`] rather than a join.
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn join_private_room(&self, room: &str) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_join_room(room, true))
|
|
}
|
|
|
|
/// Invite `username` into the private room we own (code 134).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn add_room_member(&self, room: &str, username: &str) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_add_room_member(
|
|
room, username,
|
|
))
|
|
}
|
|
|
|
/// Remove `username` from the private room we own (code 135).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn remove_room_member(&self, room: &str, username: &str) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_remove_room_member(
|
|
room, username,
|
|
))
|
|
}
|
|
|
|
/// Give up our own membership of a private room (code 136).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn leave_private_room(&self, room: &str) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_cancel_room_membership(
|
|
room,
|
|
))
|
|
}
|
|
|
|
/// Give up ownership of a private room, which deletes it (code 137).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn disband_private_room(&self, room: &str) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_cancel_room_ownership(
|
|
room,
|
|
))
|
|
}
|
|
|
|
/// Say whether we accept private-room invitations (code 141).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn set_room_invitations_enabled(&self, enabled: bool) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_enable_room_invitations(
|
|
enabled,
|
|
))
|
|
}
|
|
|
|
/// Make `username` an operator of our private room (code 143).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn add_room_operator(&self, room: &str, username: &str) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_add_room_operator(
|
|
room, username,
|
|
))
|
|
}
|
|
|
|
/// Take operator status away from `username` (code 144).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn remove_room_operator(
|
|
&self,
|
|
room: &str,
|
|
username: &str,
|
|
) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_remove_room_operator(
|
|
room, username,
|
|
))
|
|
}
|
|
|
|
/// Give up our own operator status in `room` (code 147).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn resign_room_operatorship(&self, room: &str) -> Result<()> {
|
|
self.send_server_message(
|
|
MessageFactory::build_cancel_room_operatorship(room),
|
|
)
|
|
}
|
|
|
|
/// Who may enter the private room `room`, as the server last reported.
|
|
#[must_use]
|
|
pub fn private_room_members(&self, room: &str) -> Vec<String> {
|
|
match self.context.read_safe() {
|
|
Ok(ctx) => ctx.private_room_members(room),
|
|
Err(e) => {
|
|
error!("[client] private_room_members: {}", e);
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Who runs the private room `room`.
|
|
#[must_use]
|
|
pub fn private_room_operators(&self, room: &str) -> Vec<String> {
|
|
match self.context.read_safe() {
|
|
Ok(ctx) => ctx.private_room_operators(room),
|
|
Err(e) => {
|
|
error!("[client] private_room_operators: {}", e);
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The private rooms we belong to.
|
|
#[must_use]
|
|
pub fn private_rooms(&self) -> Vec<String> {
|
|
match self.context.read_safe() {
|
|
Ok(ctx) => ctx.private_rooms(),
|
|
Err(e) => {
|
|
error!("[client] private_rooms: {}", e);
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Set our online status (server code 28): away, or back to online.
|
|
///
|
|
/// Other clients see it in their `GetUserStatus` and watch replies, and
|
|
/// the server uses it when it decides who to hand a search to.
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn set_away(&self, away: bool) -> Result<()> {
|
|
let status = u32::from(!away) + 1; // away = 1, online = 2
|
|
self.send_server_message(MessageFactory::build_set_status_message(
|
|
status,
|
|
))
|
|
}
|
|
|
|
/// Ask `username` for the contents of one folder of their shares (peer
|
|
/// code 36) — what "download folder" in other clients asks for, without
|
|
/// pulling their whole listing.
|
|
///
|
|
/// The answer arrives asynchronously; take it with
|
|
/// [`Client::take_folder_contents`].
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection,
|
|
/// or [`crate::SoulseekRs::LockPoisoned`] if the client state is poisoned.
|
|
pub fn request_folder_contents(
|
|
&self,
|
|
username: &str,
|
|
folder: &str,
|
|
) -> Result<()> {
|
|
let request = crate::message::peer::build_folder_contents_request(
|
|
crate::client::next_search_token(),
|
|
folder,
|
|
);
|
|
self.send_to_peer_or_queue(username, request)
|
|
}
|
|
|
|
/// Remove and return the listing of `folder` from `username`, once their
|
|
/// answer to [`Client::request_folder_contents`] has arrived.
|
|
#[must_use]
|
|
pub fn take_folder_contents(
|
|
&self,
|
|
username: &str,
|
|
folder: &str,
|
|
) -> Option<Vec<crate::SharedDirectory>> {
|
|
self.context
|
|
.write_safe()
|
|
.ok()
|
|
.and_then(|mut ctx| ctx.take_folder_contents(username, folder))
|
|
}
|
|
|
|
/// Ask `username` where the file we queued with them sits (peer code 51).
|
|
///
|
|
/// The answer arrives asynchronously and lands on the download, readable
|
|
/// through [`Client::downloads`] as its queue position. A peer we have no
|
|
/// connection to is resolved and the request delivered once it is up, the
|
|
/// same way a browse request is.
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection,
|
|
/// or [`crate::SoulseekRs::LockPoisoned`] if the client state is poisoned.
|
|
pub fn request_place_in_queue(
|
|
&self,
|
|
username: &str,
|
|
filename: &str,
|
|
) -> Result<()> {
|
|
let request = MessageFactory::build_place_in_queue_request(filename);
|
|
self.send_to_peer_or_queue(username, request)
|
|
}
|
|
|
|
/// Set our ticker in `room` — the one-line message other members see.
|
|
/// Passing an empty `ticker` clears ours.
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn set_room_ticker(&self, room: &str, ticker: &str) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_set_room_ticker(
|
|
room, ticker,
|
|
))
|
|
}
|
|
|
|
/// The ticker board of `room`: what the server sent on join (code 113),
|
|
/// kept current by the later add/remove events.
|
|
#[must_use]
|
|
pub fn room_tickers(&self, room: &str) -> Vec<RoomTicker> {
|
|
match self.context.read_safe() {
|
|
Ok(ctx) => ctx.room_tickers(room),
|
|
Err(e) => {
|
|
error!("[client] room_tickers: {}", e);
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Subscribe to the global room feed (code 150): every public room message
|
|
/// on the server arrives as a [`crate::RoomEvent::GlobalMessage`].
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn join_global_room(&self) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_join_global_room())
|
|
}
|
|
|
|
/// Stop the global room feed (code 151).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn leave_global_room(&self) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_leave_global_room())
|
|
}
|
|
|
|
/// Add an interest (code 51). Interests are what the recommendation and
|
|
/// similar-user queries below are computed from.
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn add_interest(&self, item: &str) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_add_thing_i_like(item))
|
|
}
|
|
|
|
/// Drop an interest (code 52).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn remove_interest(&self, item: &str) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_remove_thing_i_like(
|
|
item,
|
|
))
|
|
}
|
|
|
|
/// Add a dislike (code 117).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn add_dislike(&self, item: &str) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_add_thing_i_hate(item))
|
|
}
|
|
|
|
/// Drop a dislike (code 118).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn remove_dislike(&self, item: &str) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_remove_thing_i_hate(
|
|
item,
|
|
))
|
|
}
|
|
|
|
/// Ask for recommendations drawn from our own interests (code 54). The
|
|
/// reply arrives asynchronously; read it with
|
|
/// [`Client::recommendations`].
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn request_recommendations(&self) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_get_recommendations())
|
|
}
|
|
|
|
/// Ask for the server-wide recommendations (code 56); read the reply with
|
|
/// [`Client::global_recommendations`].
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn request_global_recommendations(&self) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_global_recommendations())
|
|
}
|
|
|
|
/// The last reply to [`Client::request_recommendations`] as
|
|
/// (recommended, recommended-against), or `None` until one arrives.
|
|
#[must_use]
|
|
pub fn recommendations(
|
|
&self,
|
|
) -> Option<(Vec<Recommendation>, Vec<Recommendation>)> {
|
|
self.context
|
|
.read_safe()
|
|
.ok()
|
|
.and_then(|ctx| ctx.recommendations(false))
|
|
}
|
|
|
|
/// The last reply to [`Client::request_global_recommendations`].
|
|
#[must_use]
|
|
pub fn global_recommendations(
|
|
&self,
|
|
) -> Option<(Vec<Recommendation>, Vec<Recommendation>)> {
|
|
self.context
|
|
.read_safe()
|
|
.ok()
|
|
.and_then(|ctx| ctx.recommendations(true))
|
|
}
|
|
|
|
/// Ask what else people who like `item` like (code 111).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn request_item_recommendations(&self, item: &str) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_item_recommendations(
|
|
item,
|
|
))
|
|
}
|
|
|
|
/// The last per-item recommendations received for `item`.
|
|
#[must_use]
|
|
pub fn item_recommendations(&self, item: &str) -> Vec<Recommendation> {
|
|
match self.context.read_safe() {
|
|
Ok(ctx) => ctx.item_recommendations(item),
|
|
Err(e) => {
|
|
error!("[client] item_recommendations: {}", e);
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Ask which users share our interests (code 110).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn request_similar_users(&self) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_similar_users())
|
|
}
|
|
|
|
/// The last similar-user answer, weighted by how much we have in common.
|
|
#[must_use]
|
|
pub fn similar_users(&self) -> Vec<SimilarUser> {
|
|
match self.context.read_safe() {
|
|
Ok(ctx) => ctx.similar_users(),
|
|
Err(e) => {
|
|
error!("[client] similar_users: {}", e);
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Ask which users like `item` (code 112).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn request_item_similar_users(&self, item: &str) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_item_similar_users(item))
|
|
}
|
|
|
|
/// The last answer to [`Client::request_item_similar_users`] for `item`.
|
|
#[must_use]
|
|
pub fn item_similar_users(&self, item: &str) -> Vec<String> {
|
|
match self.context.read_safe() {
|
|
Ok(ctx) => ctx.item_similar_users(item),
|
|
Err(e) => {
|
|
error!("[client] item_similar_users: {}", e);
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Ask what `username` likes and hates (code 57).
|
|
///
|
|
/// Any previous answer for `username` is dropped first, so
|
|
/// [`Client::user_interests`] reports the reply to *this* request rather
|
|
/// than a stale one.
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn request_user_interests(&self, username: &str) -> Result<()> {
|
|
self.context
|
|
.write_safe()?
|
|
.invalidate_user_interests(username);
|
|
self.send_server_message(MessageFactory::build_user_interests(username))
|
|
}
|
|
|
|
/// What the server last told us `username` likes and hates.
|
|
#[must_use]
|
|
pub fn user_interests(&self, username: &str) -> Option<UserInterests> {
|
|
self.context
|
|
.read_safe()
|
|
.ok()
|
|
.and_then(|ctx| ctx.user_interests(username))
|
|
}
|
|
|
|
/// Give `days` of our own privileges to `username` (code 123).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn give_privileges(&self, username: &str, days: u32) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_give_privileges(
|
|
username, days,
|
|
))
|
|
}
|
|
|
|
/// Change our account password (code 142).
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn change_password(&self, password: &str) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_change_password(
|
|
password,
|
|
))
|
|
}
|
|
|
|
/// Send one private message to several users at once (code 149). Each
|
|
/// recipient receives it as an ordinary private message.
|
|
///
|
|
/// # Errors
|
|
/// [`crate::SoulseekRs::NotConnected`] when there is no server connection.
|
|
pub fn send_private_message_to_many(
|
|
&self,
|
|
usernames: &[String],
|
|
message: &str,
|
|
) -> Result<()> {
|
|
self.send_server_message(MessageFactory::build_message_users(
|
|
usernames, message,
|
|
))
|
|
}
|
|
}
|