Merge pull request #75 from michel/lib/admin-message

feat(lib): surface server announcements as messages from "server"
This commit is contained in:
Michel de Graaf
2026-09-07 17:26:30 +02:00
committed by GitHub
4 changed files with 81 additions and 8 deletions
@@ -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;
+47 -8
View File
@@ -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");
}