1#![doc = include_str!("../readme.md")]
2
3use std::net::{IpAddr, Ipv4Addr};
4use std::sync::{Arc, Mutex};
5use std::time::Duration;
6
7use futures_util::Stream;
8use irc::client::Client;
9use irc::client::data::Config;
10use irc::error::{Error, Result};
11use irc::proto::Message;
12use names::Generator;
13
14struct InnerEngine {
16 names: Mutex<Generator<'static>>,
18 timeout: Duration,
20}
21
22impl Default for InnerEngine {
23 fn default() -> Self {
24 Self {
25 names: Default::default(),
26 timeout: Duration::from_secs(30),
27 }
28 }
29}
30
31impl std::fmt::Debug for InnerEngine {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.debug_struct(stringify!(InnerEngine))
34 .field("timeout", &self.timeout)
35 .finish_non_exhaustive()
36 }
37}
38
39impl InnerEngine {
40 fn next_name(&self) -> Option<String> {
42 if let Ok(mut lock) = self.names.lock() {
43 lock.next()
44 } else {
45 None
46 }
47 }
48}
49
50#[derive(Clone, Debug, Default)]
52pub struct Engine(Arc<InnerEngine>);
53
54impl Engine {
55 pub fn create_request(
64 &self,
65 server: impl Into<String>,
66 channel: impl Into<String>,
67 botname: impl Into<String>,
68 packnum: u64,
69 ) -> Request {
70 Request {
71 inner: self.0.clone(),
72 info: RequestInfo {
73 server: server.into(),
74 channel: channel.into(),
75 botname: botname.into(),
76 packnum,
77 },
78 }
79 }
80}
81
82#[derive(Clone, Debug)]
84pub struct RequestInfo {
85 pub server: String,
87 pub channel: String,
89 pub botname: String,
91 pub packnum: u64,
93}
94
95#[derive(Debug)]
97pub struct Request {
98 inner: Arc<InnerEngine>,
99 info: RequestInfo,
100}
101
102async fn wait_for_first_private_message(
106 mut stream: impl Stream<Item = Result<Message>> + Unpin,
107) -> Result<()> {
108 use futures_util::StreamExt;
109
110 while let Some(message) = stream.next().await.transpose()? {
111 if matches!(message.command, irc::proto::Command::PRIVMSG(_, _)) {
112 return Ok(());
113 }
114 }
115
116 Err(Error::AsyncChannelClosed)
117}
118
119async fn wait_for_dcc_response(
123 mut stream: impl Stream<Item = Result<Message>> + Unpin,
124) -> Result<Response> {
125 use futures_util::StreamExt;
126
127 while let Some(message) = stream.next().await.transpose()? {
128 let irc::proto::Command::PRIVMSG(_botname, cmd) = message.command else {
129 continue;
130 };
131 if let Some(res) = Response::decode(&cmd) {
132 return Ok(res);
133 }
134 }
135
136 Err(Error::AsyncChannelClosed)
137}
138
139impl Request {
140 pub async fn execute(&self) -> Result<Response> {
148 let config = Config {
149 nickname: self.inner.next_name(),
150 server: Some(self.info.server.clone()),
151 channels: vec![self.info.channel.clone()],
152 ..Default::default()
153 };
154
155 let mut client = Client::from_config(config).await?;
156 client.identify()?;
157
158 let mut stream = client.stream()?;
159 tokio::time::timeout(
160 self.inner.timeout,
161 wait_for_first_private_message(&mut stream),
162 )
163 .await
164 .map_err(|_| Error::PingTimeout)??;
165
166 client.send_privmsg(
167 self.info.botname.as_str(),
168 format!("xdcc send #{}", self.info.packnum),
169 )?;
170
171 tokio::time::timeout(self.inner.timeout, wait_for_dcc_response(&mut stream))
172 .await
173 .map_err(|_| Error::PingTimeout)?
174 }
175}
176
177#[derive(Clone, Debug)]
179pub struct Response {
180 pub filename: String,
182 pub address: IpAddr,
184 pub port: u16,
186 pub filesize: u64,
188}
189
190impl Response {
191 pub fn decode(msg: &str) -> Option<Self> {
195 let msg = msg.trim().strip_prefix("DCC SEND ")?;
196
197 let (msg, filesize) = msg.rsplit_once(" ")?;
198 let filesize = filesize.parse::<u64>().ok()?;
199
200 let (msg, port) = msg.rsplit_once(" ")?;
201 let port = port.parse::<u16>().ok()?;
202
203 let (msg, ip) = msg.rsplit_once(" ")?;
204 let ip = ip.parse::<u32>().ok()?;
205 let ip = Ipv4Addr::from(ip);
206
207 let filename = msg.trim_matches('"');
208 let filename = filename.replace("\\\"", "\"");
209
210 Some(Self {
211 filename,
212 address: IpAddr::V4(ip),
213 port,
214 filesize,
215 })
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use futures_util::stream;
222 use irc::proto::{Command, Message};
223
224 #[tokio::test]
225 async fn should_wait_for_dcc_message() {
226 let mut stream = stream::iter(vec![Ok(Message {
227 tags: None,
228 prefix: None,
229 command: Command::PRIVMSG(
230 "botname".into(),
231 "DCC SEND \"ubuntu.iso\" 3232235777 5000 1048576".into(),
232 ),
233 })]);
234 let res = super::wait_for_dcc_response(&mut stream).await.unwrap();
235 assert_eq!(res.filename, "ubuntu.iso");
236 }
237
238 #[tokio::test]
239 async fn should_wait_for_private_message() {
240 let mut stream = stream::iter(vec![
241 Ok(Message {
242 tags: None,
243 prefix: None,
244 command: Command::PING(Default::default(), Default::default()),
245 }),
246 Ok(Message {
247 tags: None,
248 prefix: None,
249 command: Command::PRIVMSG("botname".into(), "hello world".into()),
250 }),
251 ]);
252 super::wait_for_first_private_message(&mut stream)
253 .await
254 .unwrap();
255 }
256
257 #[tokio::test]
258 async fn should_fail_if_no_private_message() {
259 let mut stream = stream::iter(vec![Ok(Message {
260 tags: None,
261 prefix: None,
262 command: Command::PING(Default::default(), Default::default()),
263 })]);
264 super::wait_for_first_private_message(&mut stream)
265 .await
266 .unwrap_err();
267 }
268
269 #[test_case::test_case("DCC SEND \"foo.txt\" 3232235777 5000 1048576", "foo.txt", 5000, 1048576; "simple")]
270 #[test_case::test_case("DCC SEND \"hello\\\"world.txt\" 3232235777 5000 1048576", "hello\"world.txt", 5000, 1048576; "with quotes")]
271 #[test_case::test_case("DCC SEND \"foo bar baz.txt\" 3232235777 5000 1048576", "foo bar baz.txt", 5000, 1048576; "filename with spaces")]
272 fn should_decode_dcc_msg(msg: &str, fname: &str, port: u16, size: u64) {
273 let res = super::Response::decode(msg).unwrap();
274 assert_eq!(res.filename, fname);
275 assert_eq!(res.port, port);
276 assert_eq!(res.filesize, size);
277 }
278}