Merge pull request #75 from michel/lib/admin-message
feat(lib): surface server announcements as messages from "server"
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
use crate::actor::{Actor, ActorHandle, ConnectionState};
|
use crate::actor::{Actor, ActorHandle, ConnectionState};
|
||||||
use crate::client::ClientOperation;
|
use crate::client::ClientOperation;
|
||||||
use crate::dispatcher::MessageDispatcher;
|
use crate::dispatcher::MessageDispatcher;
|
||||||
|
use crate::message::server::AdminMessageHandler;
|
||||||
use crate::message::server::CheckPrivilegesHandler;
|
use crate::message::server::CheckPrivilegesHandler;
|
||||||
use crate::message::server::ConnectToPeerHandler;
|
use crate::message::server::ConnectToPeerHandler;
|
||||||
use crate::message::server::ExcludedSearchPhrasesHandler;
|
use crate::message::server::ExcludedSearchPhrasesHandler;
|
||||||
@@ -383,6 +384,7 @@ impl ServerActor {
|
|||||||
|
|
||||||
handlers.register_handler(LoginHandler);
|
handlers.register_handler(LoginHandler);
|
||||||
handlers.register_handler(ReloggedHandler);
|
handlers.register_handler(ReloggedHandler);
|
||||||
|
handlers.register_handler(AdminMessageHandler);
|
||||||
handlers.register_handler(RoomListHandler);
|
handlers.register_handler(RoomListHandler);
|
||||||
handlers.register_handler(GetUserStatusHandler);
|
handlers.register_handler(GetUserStatusHandler);
|
||||||
handlers.register_handler(WatchUserHandler);
|
handlers.register_handler(WatchUserHandler);
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//! `AdminMessage` (server code 66): an announcement from the server to
|
||||||
|
//! everyone, surfaced as a private message from the user "server".
|
||||||
|
|
||||||
|
use crate::actor::server_actor::{ServerMessage, UserMessage};
|
||||||
|
use crate::message::{Message, MessageHandler};
|
||||||
|
use std::sync::mpsc::Sender;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
pub struct AdminMessageHandler;
|
||||||
|
|
||||||
|
impl MessageHandler<ServerMessage> for AdminMessageHandler {
|
||||||
|
fn get_code(&self) -> u32 {
|
||||||
|
66
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle(&self, message: &mut Message, sender: Sender<ServerMessage>) {
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map_or(0, |since| since.as_secs() as u32);
|
||||||
|
let announcement = UserMessage::new(
|
||||||
|
0,
|
||||||
|
now,
|
||||||
|
"server".to_string(),
|
||||||
|
message.read_string(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
let _ =
|
||||||
|
sender.send(ServerMessage::PrivateMessageReceived(announcement));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod admin_message;
|
||||||
mod connect_to_peer;
|
mod connect_to_peer;
|
||||||
mod excluded_search_phrases;
|
mod excluded_search_phrases;
|
||||||
mod file_search;
|
mod file_search;
|
||||||
@@ -19,6 +20,7 @@ mod user_left_room;
|
|||||||
mod watch_user;
|
mod watch_user;
|
||||||
mod wish_list_interval;
|
mod wish_list_interval;
|
||||||
|
|
||||||
|
pub use admin_message::AdminMessageHandler;
|
||||||
pub use connect_to_peer::ConnectToPeerHandler;
|
pub use connect_to_peer::ConnectToPeerHandler;
|
||||||
pub use excluded_search_phrases::ExcludedSearchPhrasesHandler;
|
pub use excluded_search_phrases::ExcludedSearchPhrasesHandler;
|
||||||
pub use file_search::FileSearchHandler;
|
pub use file_search::FileSearchHandler;
|
||||||
|
|||||||
@@ -2348,23 +2348,24 @@ fn place_of(sharer: &Client, username: &str, filename: &str) -> Option<u32> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Give `username` privileges in soulfind's own database.
|
/// Set `column` ("privileges", or "admin" for the user whose private
|
||||||
|
/// messages to "server" are commands) on `username` in soulfind's own
|
||||||
|
/// database, good for a day: soulfind stores both as the unix timestamp
|
||||||
|
/// they expire at.
|
||||||
///
|
///
|
||||||
/// `false` means it could not be done — an external server, or no `sqlite3` on
|
/// `false` means it could not be done — an external server, or no `sqlite3`
|
||||||
/// this machine — which the caller treats as a skip rather than a failure.
|
/// on this machine — which the caller treats as a skip rather than a failure.
|
||||||
fn grant_privileges(server: &TestServer, username: &str) -> bool {
|
fn grant(server: &TestServer, username: &str, column: &str) -> bool {
|
||||||
let Some(db) = server.db.as_ref() else {
|
let Some(db) = server.db.as_ref() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
// soulfind stores privileges as the unix timestamp they expire at.
|
|
||||||
let expiry = std::time::SystemTime::now()
|
let expiry = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.map_or(0, |since| since.as_secs() + 86_400);
|
.map_or(0, |since| since.as_secs() + 86_400);
|
||||||
Command::new("sqlite3")
|
Command::new("sqlite3")
|
||||||
.arg(db)
|
.arg(db)
|
||||||
.arg(format!(
|
.arg(format!(
|
||||||
"UPDATE users SET privileges = {expiry} WHERE username = \
|
"UPDATE users SET {column} = {expiry} WHERE username = '{username}';"
|
||||||
'{username}';"
|
|
||||||
))
|
))
|
||||||
.status()
|
.status()
|
||||||
.is_ok_and(|status| status.success())
|
.is_ok_and(|status| status.success())
|
||||||
@@ -2473,7 +2474,7 @@ fn a_privileged_peer_overtakes_one_already_waiting_for_a_slot() {
|
|||||||
login_raw(&server_addr, "e2e_q_donor", "pw")
|
login_raw(&server_addr, "e2e_q_donor", "pw")
|
||||||
.expect("the donor registers"),
|
.expect("the donor registers"),
|
||||||
);
|
);
|
||||||
if !grant_privileges(&server, "e2e_q_donor") {
|
if !grant(&server, "e2e_q_donor", "privileges") {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"privilege e2e skipped: cannot write soulfind's database (needs a \
|
"privilege e2e skipped: cannot write soulfind's database (needs a \
|
||||||
locally spawned server and sqlite3)"
|
locally spawned server and sqlite3)"
|
||||||
@@ -3423,3 +3424,41 @@ fn a_large_listing_travels_compressed() {
|
|||||||
|
|
||||||
let _ = std::fs::remove_dir_all(share_dir);
|
let _ = std::fs::remove_dir_all(share_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The server's announcements (code 66) were dropped on the floor; they are
|
||||||
|
// how a server tells everyone it is going down.
|
||||||
|
#[test]
|
||||||
|
fn a_server_announcement_arrives_as_a_message_from_the_server() {
|
||||||
|
let server = server_or_skip!();
|
||||||
|
|
||||||
|
let mut admin = Client::with_settings(server.settings("e2e_admin", "pw"));
|
||||||
|
let mut bob =
|
||||||
|
Client::with_settings(server.settings("e2e_announce_bob", "pw"));
|
||||||
|
admin.connect().expect("admin connect");
|
||||||
|
bob.connect().expect("bob connect");
|
||||||
|
assert!(admin.login().expect("admin login"));
|
||||||
|
assert!(bob.login().expect("bob login"));
|
||||||
|
if !grant(&server, "e2e_admin", "admin") {
|
||||||
|
println!("e2e skipped: cannot make an admin on this server");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
admin
|
||||||
|
.send_private_message("server", "announcement going down at nine")
|
||||||
|
.expect("send the admin command");
|
||||||
|
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(5);
|
||||||
|
let mut received = Vec::new();
|
||||||
|
while Instant::now() < deadline {
|
||||||
|
received.extend(bob.take_private_messages());
|
||||||
|
if received.iter().any(|m| m.username() == "server") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
let announcement = received
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.username() == "server")
|
||||||
|
.expect("bob should hear the announcement");
|
||||||
|
assert_eq!(announcement.message(), "going down at nine");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user