aboutsummaryrefslogtreecommitdiffstats
path: root/src/client/mod.rs
blob: 6d32017f4d8cc4cc01da0414f208a9cfae4eeec0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
use tokio::sync::mpsc;
use tracing::info;

use crate::client::event::Event;
use crate::client::handler::handle;
use crate::client::state::ClientState;
use crate::connection::{self, Sender};
use crate::proto::message::{Command, IrcMessage};

pub mod event;
pub mod handler;
pub mod state;

/// Configuration for the IRC client.
pub struct Config {
    /// Server address, e.g. "irc.libera.chat:6667"
    pub server: String,
    /// Desired nick
    pub nick: String,
    /// IRC username (shown in /whois)
    pub user: String,
    /// Real name (shown in /whois)
    pub realname: String,
    /// Optional server password
    pub password: Option<String>,
    /// Channels to auto-join after registration
    pub autojoin: Vec<String>,
}

/// The main IRC client.
///
/// Call `Client::connect` to establish a connection, then drive the event
/// loop with `client.next_event().await` in your application loop.
pub struct Client {
    state: ClientState,
    sender: Sender,
    inbox: mpsc::UnboundedReceiver<IrcMessage>,
    config: Config,
}

impl Client {
    /// Connect to the server and begin the registration handshake.
    pub async fn connect(config: Config) -> Result<Self, std::io::Error> {
        let (sender, inbox) = connection::connect(&config.server).await?;
        let state = ClientState::new(&config.nick);

        let client = Self {
            state,
            sender,
            inbox,
            config,
        };
        client.register();
        Ok(client)
    }

    /// Send a raw `IrcMessage` to the server.
    pub fn send(&self, msg: IrcMessage) {
        self.sender.send(msg);
    }

    /// Send a PRIVMSG to a channel or user.
    pub fn privmsg(&self, target: &str, text: &str) {
        self.sender.send(IrcMessage::new(
            Command::Privmsg,
            vec![target.to_string(), text.to_string()],
        ));
    }

    /// Join a channel.
    pub fn join(&self, channel: &str) {
        self.sender
            .send(IrcMessage::new(Command::Join, vec![channel.to_string()]));
    }

    /// Part a channel.
    pub fn part(&self, channel: &str, reason: Option<&str>) {
        let mut params = vec![channel.to_string()];
        if let Some(r) = reason {
            params.push(r.to_string());
        }
        self.sender.send(IrcMessage::new(Command::Part, params));
    }

    /// Change nick.
    pub fn nick(&self, new_nick: &str) {
        self.sender
            .send(IrcMessage::new(Command::Nick, vec![new_nick.to_string()]));
    }

    /// Read-only view of current client state.
    pub fn state(&self) -> &ClientState {
        &self.state
    }

    /// Wait for the next event from the server.
    /// Returns `None` if the connection has closed.
    pub async fn next_event(&mut self) -> Option<Event> {
        loop {
            let msg = self.inbox.recv().await?;
            let events = handle(msg, &mut self.state, &self.sender);

            // Handle auto-join after registration
            for event in &events {
                if let Event::Connected { .. } = event {
                    for channel in &self.config.autojoin.clone() {
                        info!("Auto-joining {}", channel);
                        self.join(channel);
                    }
                }
            }

            // Return the first event; re-queue the rest
            // (simple approach: process one at a time via recursive buffering)
            if let Some(first) = events.into_iter().next() {
                return Some(first);
            }
            // If no events were produced (e.g. a PING), loop and wait for next message
        }
    }

    /// Send the registration sequence to the server.
    fn register(&self) {
        // Optional server password
        if let Some(pass) = &self.config.password {
            self.sender
                .send(IrcMessage::new(Command::Pass, vec![pass.clone()]));
        }

        // Begin CAP negotiation first — lets us request IRCv3 caps
        // before NICK/USER so the server doesn't rush past registration
        self.sender.send(IrcMessage::new(
            Command::Cap,
            vec!["LS".into(), "302".into()],
        ));

        self.sender.send(IrcMessage::new(
            Command::Nick,
            vec![self.config.nick.clone()],
        ));

        self.sender.send(IrcMessage::new(
            Command::User,
            vec![
                self.config.user.clone(),
                "0".into(),
                "*".into(),
                self.config.realname.clone(),
            ],
        ));
    }
}

impl Client {
    /// Non-blocking version of `next_event`.
    /// Returns `Some(event)` if one is immediately available, `None` otherwise.
    /// Used by the TUI loop to drain events without blocking the render tick.
    pub fn next_event_nowait(&mut self) -> Option<Event> {
        loop {
            let msg = self.inbox.try_recv().ok()?;
            let mut events = handle(msg, &mut self.state, &self.sender);

            for event in &events {
                if let Event::Connected { .. } = event {
                    for channel in &self.config.autojoin.clone() {
                        self.join(channel);
                    }
                }
            }

            if !events.is_empty() {
                return Some(events.remove(0));
            }
        }
    }
}