diff --git a/examples/hello_libera.rs b/examples/hello_libera.rs index e6f0800..4fd7a1f 100644 --- a/examples/hello_libera.rs +++ b/examples/hello_libera.rs @@ -47,14 +47,17 @@ fn main() -> std::io::Result<()> { // so we can ignore it. client.run()?; // If we're here, the handler finished and gave us a value back. - // Let's fetch it and see what it is! - let reg = reg_result.0.recv_now().unwrap()?; + // The registration handler only tells us what the error was, if it failed. + // So, let's check to see if we made it. + reg_result.0.recv_now().unwrap()?; // Connection registration is done! - // But how does the network we connected to choose to name itself? - // ISUPPORT is vital for understanding the capabilities of the target network, - // and vinezombie eagerly parses it during registration. Let's print the network name. - let network_name = reg.isupport.get_parsed(vinezombie::names::isupport::NETWORK).transpose()?; - tracing::info!("{} connected to {}!", reg.nick, network_name.unwrap_or(Word::from_str("IRC"))); + // Now, what is our nick? `Client` can store information about the current session + // for use by other handlers. We can access that information through the `state` method. + // While we're at it, let's get the network name from ISUPPORT. + let nick = &client.state().get::().unwrap().nick; + let isupport = client.state().get::().unwrap(); + let network_name = isupport.get_parsed(vinezombie::names::isupport::NETWORK).transpose()?; + tracing::info!("{} connected to {}!", nick, network_name.unwrap_or(Word::from_str("IRC"))); // From here, we can keep reading messages (including 004 and 005) // but we don't care about any of that, so let's just quit. // We create the message, push it onto the internal message queue, diff --git a/examples/hello_libera_tokio.rs b/examples/hello_libera_tokio.rs index 9b9e11a..bb361c1 100644 --- a/examples/hello_libera_tokio.rs +++ b/examples/hello_libera_tokio.rs @@ -27,10 +27,12 @@ async fn main() -> std::io::Result<()> { // but instead we run it using a run_tokio function. let (_id, reg_result) = client.add(®ister_as_bot(), &options).unwrap(); client.run_tokio().await?; - let reg = reg_result.await.unwrap()?; - let network_name = reg.isupport.get_parsed(vinezombie::names::isupport::NETWORK).transpose()?; - tracing::info!("{} connected to {}!", reg.nick, network_name.unwrap_or(Word::from_str("IRC"))); - // As with the earlier example, let's just quit here. + reg_result.await.unwrap()?; + // Almost everything past this point is the same as the sync example. + let nick = &client.state().get::().unwrap().nick; + let isupport = client.state().get::().unwrap(); + let network_name = isupport.get_parsed(vinezombie::names::isupport::NETWORK).transpose()?; + tracing::info!("{} connected to {}!", nick, network_name.unwrap_or(Word::from_str("IRC"))); let msg = ClientMsg::new(vinezombie::names::cmd::QUIT); client.queue_mut().edit().push(msg); client.run_tokio().await?; diff --git a/examples/msglog.rs b/examples/msglog.rs index 874e135..1c7f8d4 100644 --- a/examples/msglog.rs +++ b/examples/msglog.rs @@ -6,6 +6,7 @@ use vinezombie::{ conn::ServerAddr, handlers::{AutoPong, YieldParsed}, register::{register_as_bot, Options}, + state::ClientSource, Client, }, ircmsg::ClientMsg, @@ -30,8 +31,9 @@ async fn main() -> std::io::Result<()> { let mut client = Client::new(sock, TokioChannels); let (_id, reg_result) = client.add(®ister_as_bot(), &options).unwrap(); client.run_tokio().await?; + reg_result.await.unwrap()?; // The only piece of reg info we care about for this example is our nick. - let nick = reg_result.await.unwrap()?.nick; + let nick = client.state().get::().unwrap().nick.clone(); tracing::info!("nick: {}", nick); // Let's add a handler to auto-reply to PING messages for us. Most IRC networks need this. // Do this before anything else. diff --git a/examples/reconnect.rs b/examples/reconnect.rs index 8600e92..64d9d8a 100644 --- a/examples/reconnect.rs +++ b/examples/reconnect.rs @@ -7,6 +7,7 @@ use vinezombie::{ conn::{ServerAddr, Stream}, handlers::{AutoPong, YieldParsed}, register::{register_as_bot, Options}, + state::ClientSource, tls::TlsConfig, Client, }, @@ -49,7 +50,7 @@ fn main() -> std::io::Result<()> { loop { let (_, reg_result) = client.add(®ister_as_bot(), &options).unwrap(); client.run()?; - let nick = reg_result.0.recv_now().unwrap()?.nick; + reg_result.0.recv_now().unwrap()?; let _ = client.add((), AutoPong); // As we can interact with this bot, let's add a handler to auto-reply to // CTCP VERSION and CTCP SOURCE. @@ -61,7 +62,7 @@ fn main() -> std::io::Result<()> { // This time we're actually going to use that information, // starting by saving the id of the message handler. let (id, msgs) = client.add((), YieldParsed::just(PRIVMSG)).unwrap(); - tracing::info!("bot {nick} ready for 'q'~"); + tracing::info!("bot {} ready for 'q'~", client.state().get::().unwrap().nick); loop { let Ok(result) = client.run() else { tracing::info!("connection broke, making new connection"); diff --git a/examples/sasl.rs b/examples/sasl.rs index a32cb7c..6eb5b2b 100644 --- a/examples/sasl.rs +++ b/examples/sasl.rs @@ -5,6 +5,7 @@ use vinezombie::{ auth::{sasl::Password, Clear, Secret}, channel::TokioChannels, register::{register_as_bot, Options}, + state::Account, Client, }, string::{tf::TrimAscii, Line, NoNul}, @@ -46,9 +47,9 @@ async fn main() -> std::io::Result<()> { let mut client = Client::new(sock, TokioChannels); let (_id, reg_result) = client.add(®ister_as_bot(), &options).unwrap(); client.run_tokio().await?; - let reg = reg_result.await.unwrap()?; + reg_result.await.unwrap()?; // Who'd we log in as? - if let Some(account) = reg.account { + if let Some(account) = client.state().get::().unwrap() { tracing::info!("Logged in as {account}"); } else { // We should never get here unless `options.allow_sasl_fail` is set to `true`. diff --git a/src/client/register.rs b/src/client/register.rs index 9721d67..0531deb 100644 --- a/src/client/register.rs +++ b/src/client/register.rs @@ -21,6 +21,11 @@ use crate::{ /// /// Consider using the [`register_as_bot()`], [`register_as_client()`], /// or [`register_as_custom()`] functions to instantiate one of these. +/// +/// The handler returned by using this type signals completion over its channel. +/// Most of the useful data about client registration is added to the client as shared state. +/// In particular, connection statistics and the MOTD are NOT stored, +/// and should be read using a different handler. #[derive(Clone)] pub struct Register { /// Returns the server password, if any. @@ -110,7 +115,7 @@ impl Register { } impl<'a, O> MakeHandler<&'a O> for &'a Register { - type Value = Result; + type Value = Result<(), HandlerError>; type Error = std::convert::Infallible; diff --git a/src/client/register/handler.rs b/src/client/register/handler.rs index 6a78654..1f3d355 100644 --- a/src/client/register/handler.rs +++ b/src/client/register/handler.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; +use super::CapFn; use crate::{ client::{ auth::{self, SaslQueue}, @@ -14,9 +15,7 @@ use crate::{ string::{Arg, Key, Line, Nick, Splitter, Word}, }; -use super::CapFn; - -/// The result of successful registration. +/// A useful subset of information yielded by client registration. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub struct Registration { /// The nickname used for this connection. @@ -55,6 +54,21 @@ impl Registration { isupport: NameMap::new(), } } + /// Saves registration to a [`ClientState`][crate::client::ClientState]. + pub fn save(self, state: &mut crate::client::ClientState) { + use crate::client::state::*; + let source = Source { nick: self.nick, userhost: self.userhost }; + state.insert::(source); + state.insert::(self.account); + state.insert::(self.caps); + state.insert::(self.isupport); + if let Some(server_source) = self.source { + state.insert::(server_source); + } + if let Some(version) = self.version { + state.insert::(version); + } + } } impl Registration { @@ -224,11 +238,7 @@ impl Handler { reg: Registration::new(nick), } } - /// Handles a server message sent during connection registration. - /// - /// It is a logic error to call `handle` after - /// it errors or returns `Ok(Done)`. - pub fn handle( + fn handle( &mut self, msg: &ServerMsg<'_>, mut sink: impl ClientMsgSink<'static>, @@ -515,18 +525,19 @@ impl Handler { } impl crate::client::Handler for Handler { - type Value = Result; + type Value = Result<(), HandlerError>; fn handle( &mut self, msg: &ServerMsg<'_>, - _: &mut crate::client::ClientState, + state: &mut crate::client::ClientState, mut queue: crate::client::queue::QueueEditGuard<'_>, mut channel: crate::client::channel::SenderRef<'_, Self::Value>, ) -> bool { match self.handle(msg, &mut queue) { Ok(Some(v)) => { - channel.send(Ok(v)); + v.save(state); + channel.send(Ok(())); true } Ok(None) => false, diff --git a/src/client/register/tests.rs b/src/client/register/tests.rs index fd66cb0..4f64f15 100644 --- a/src/client/register/tests.rs +++ b/src/client/register/tests.rs @@ -1,14 +1,19 @@ use std::{io::Cursor, time::Duration}; +use super::{register_as_bot, HandlerError, Options}; use crate::{ - client::{auth::Clear, channel::SyncChannels, conn::Bidir, Client}, + client::{ + auth::Clear, + channel::SyncChannels, + conn::Bidir, + state::{Caps, ISupport}, + Client, ClientState, + }, string::{Key, Nick}, }; -use super::{register_as_bot, HandlerError, Options, Registration}; - /// Test registration while ignoring the messages the handler sends. -fn static_register(msg: &[u8]) -> Result { +fn static_register(msg: &[u8]) -> Result { let mut options: Options = Options::new(); options.nicks = vec![Nick::from_str("Me")]; let reg = register_as_bot(); // Somewhat more deterministic. @@ -17,7 +22,8 @@ fn static_register(msg: &[u8]) -> Result { client.queue_mut().set_rate_limit(Duration::ZERO, 1); let (_, reg) = client.add(®, &options).unwrap(); client.run().unwrap(); - reg.0.recv_now().unwrap() + reg.0.recv_now().expect("Handler should send on channel after success")?; + Ok(std::mem::take(client.state_mut())) } #[test] @@ -45,7 +51,7 @@ fn ircv3_reg_simple() { // TODO: Test more thoroughly. // We should be able to handle any values for messages 001 through 003, // so we're just going to put silliness here. - let reg = static_register( + let state = static_register( concat!( ":example.com CAP * LS :quickbrownfox/lazydogjumping labeled-response\r\n", ":example.com CAP * ACK :labeled-response\r\n", @@ -61,12 +67,14 @@ fn ircv3_reg_simple() { .as_bytes(), ) .expect("ircv3 reg failed"); - assert_eq!(reg.caps.get_extra(LABELED_RESPONSE).copied(), Some(true)); + let caps = state.get::().expect("Handler should set Caps on success"); + let isupport = state.get::().expect("Handler should set ISupport on success"); + assert_eq!(caps.get_extra(LABELED_RESPONSE).copied(), Some(true)); assert_eq!( - reg.caps.get_extra_raw(&Key::from_str("quickbrownfox/lazydogjumping")).copied(), + caps.get_extra_raw(&Key::from_str("quickbrownfox/lazydogjumping")).copied(), Some(false) ); - let netname = reg.isupport.get_parsed(NETWORK).expect("NETWORK should have a value").unwrap(); + let netname = isupport.get_parsed(NETWORK).expect("NETWORK should have a value").unwrap(); assert_eq!(netname, b"example.com"); }