ts3_query/
lib.rs

1//! Ts3 query library  
2//! Small, bare-metal ts query lib without any callback support currently.
3//!
4//! A connectivity checking wrapper is available under [managed](managed) when enabling its feature.
5//!
6//! # Examples
7//! Simple auth + clients of a server group
8//! ```rust,no_run
9//! use ts3_query::*;
10//!
11//! # fn main() -> Result<(),Ts3Error> {
12//! let mut client = QueryClient::new("localhost:10011")?;
13//!
14//! client.login("serveradmin", "password")?;
15//! client.select_server_by_port(9987)?;
16//!
17//! let group_clients = client.servergroup_client_cldbids(7)?;
18//! println!("Got clients in group 7: {:?}",group_clients);
19//!
20//! client.logout()?;
21//! # Ok(())
22//! # }
23//!
24//! ```
25//!
26//! Cloning a channel
27//! ```rust, no_run
28//! use ts3_query::*;
29//!
30//! # fn main() -> Result<(),Ts3Error> {
31//! let mut client = QueryClient::new("localhost:10011")?;
32//! client.login("serveradmin", "password")?;
33//! client.select_server_by_port(9987)?;
34//!
35//! let channels = client.channels_full()?;
36//! if let Some(channel) = channels.first() {
37//!     client.create_channel(&ChannelEdit {
38//!         channel_name: Some("Cloned channel".to_owned()),
39//!         ..ChannelEdit::from(channel)
40//!     })?;
41//! }
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! Using the raw interface for setting client descriptions.
47//! ```rust,no_run
48//! use ts3_query::*;
49//!
50//! # fn main() -> Result<(),Ts3Error> {
51//! let mut client = QueryClient::new("localhost:10011")?;
52//!
53//! client.login("serveradmin", "password")?;
54//! client.select_server_by_port(9987)?;
55//!
56//! // escape things like string args, not required for clid
57//! // as it's not user input/special chars in this case
58//! let cmd = format!("clientedit clid={} client_description={}",
59//!  7, raw::escape_arg("Some Description!")
60//! );
61//! // we don't expect any value returned
62//! let _ = client.raw_command(&cmd)?;
63//!
64//! client.logout()?;
65//! # Ok(())
66//! # }
67//! ```
68//!
69//! Raw interface example retrieving online client names
70//! ```rust,no_run
71//! use ts3_query::*;
72//! use std::collections::HashSet;
73//!
74//! # fn main() -> Result<(),Ts3Error> {
75//! let mut client = QueryClient::new("localhost:10011")?;
76//!
77//! client.login("serveradmin", "password")?;
78//! client.select_server_by_port(9987)?;
79//!
80//! let mut res = raw::parse_multi_hashmap(client.raw_command("clientlist")?, false);
81//! let names = res
82//!     .into_iter()
83//!     .map(|mut e| e.remove("client_nickname")
84//!     // ignore empty value & unescape
85//!     .unwrap().map(raw::unescape_val)
86//!      // may want to catch this in a real application
87//!         .unwrap())
88//!     .collect::<HashSet<String>>();
89//! println!("{:?}",names);
90//! client.logout()?;
91//! # Ok(())
92//! # }
93//! ```
94#![cfg_attr(docsrs, feature(doc_cfg))]
95
96use snafu::{Backtrace, OptionExt, ResultExt, Snafu};
97use std::collections::HashMap;
98use std::fmt::{Debug, Write as FmtWrite};
99use std::io::{self, BufRead, BufReader, Write};
100use std::net::{Shutdown, TcpStream, ToSocketAddrs};
101use std::string::FromUtf8Error;
102use std::time::Duration;
103
104mod data;
105#[cfg_attr(docsrs, doc(cfg(feature = "managed")))]
106#[cfg(feature = "managed")]
107pub mod managed;
108pub mod raw;
109
110pub use data::*;
111use io::Read;
112use raw::*;
113use std::fmt;
114
115/// Target for message sending
116pub enum MessageTarget {
117    /// Send to client
118    Client(ClientId),
119    /// Send to current channel of this client. You have to join the channel you want to send a message to.
120    Channel,
121    /// Send to whole server
122    Server,
123}
124
125impl fmt::Display for MessageTarget {
126    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
127        match *self {
128            Self::Client(id) => write!(f, "targetmode=1 target={}", id),
129            Self::Channel => write!(f, "targetmode=2"),
130            Self::Server => write!(f, "targetmode=3"),
131        }
132    }
133}
134
135#[derive(Snafu, Debug)]
136pub enum Ts3Error {
137    /// Error on response conversion with invalid utf8 data
138    #[snafu(display("Input was invalid UTF-8: {}", source))]
139    Utf8Error { source: FromUtf8Error },
140    /// Catch-all IO error, contains optional context
141    #[snafu(display("IO Error: {}{}, kind: {:?}", context, source, source.kind()))]
142    Io {
143        /// Context of action, empty per default.
144        ///
145        /// Please use a format like `"reading connection: "`
146        context: &'static str,
147        source: io::Error,
148    },
149    /// Reached EOF reading response, server closed connection / timeout.
150    #[snafu(display("IO Error: Connection closed"))]
151    ConnectionClosed { backtrace: Backtrace },
152    #[snafu(display("No valid socket address provided."))]
153    InvalidSocketAddress { backtrace: Backtrace },
154    /// Invalid response error. Server returned unexpected data.
155    #[snafu(display("Received invalid response, {}{:?}", context, data))]
156    InvalidResponse {
157        /// Context of action, empty per default.
158        ///
159        /// Please use a format like `"expected XY, got: "`
160        context: &'static str,
161        data: String,
162    },
163    #[snafu(display("Got invalid int response {}: {}", data, source))]
164    InvalidIntResponse {
165        data: String,
166        source: std::num::ParseIntError,
167        backtrace: Backtrace,
168    },
169    /// TS3-Server error response
170    #[snafu(display("Server responded with error: {}", response))]
171    ServerError {
172        response: ErrorResponse,
173        backtrace: Backtrace,
174    },
175    /// Maximum amount of response bytes/lines reached, DDOS limit prevented further data read.
176    ///
177    /// This will probably cause the current connection to become invalid due to remaining data in the connection.
178    #[snafu(display("Invalid response, DDOS limit reached: {:?}", response))]
179    ResponseLimit {
180        response: Vec<String>,
181        backtrace: Backtrace,
182    },
183    /// Invalid name length. Client-Name is longer than allowed!
184    #[cfg(feature = "managed")]
185    #[snafu(display("Invalid name length: {} max: {}!", length, expected))]
186    InvalidNameLength { length: usize, expected: usize },
187    /// No entry for key in server response, expected one.
188    #[snafu(display("Expected entry for key {}, key not found!", key))]
189    NoEntryResponse {
190        key: &'static str,
191        backtrace: Backtrace,
192    },
193    /// No value for key in response, expected some.
194    #[snafu(display("Expected value for key {}, got none!", key))]
195    NoValueResponse {
196        key: &'static str,
197        backtrace: Backtrace,
198    },
199}
200
201impl Ts3Error {
202    /// Returns true if the error is of kind ServerError
203    pub fn is_error_response(&self) -> bool {
204        match self {
205            Ts3Error::ServerError { .. } => true,
206            _ => false,
207        }
208    }
209    /// Returns the [`ErrorResponse`](ErrorResponse) if existing.
210    pub fn error_response(&self) -> Option<&ErrorResponse> {
211        match self {
212            Ts3Error::ServerError { response, .. } => Some(response),
213            _ => None,
214        }
215    }
216}
217
218impl From<io::Error> for Ts3Error {
219    fn from(error: io::Error) -> Self {
220        Ts3Error::Io {
221            context: "",
222            source: error,
223        }
224    }
225}
226
227/// Ts3 Query client with active connection
228pub struct QueryClient {
229    rx: BufReader<TcpStream>,
230    tx: TcpStream,
231    limit_lines: usize,
232    limit_lines_bytes: u64,
233}
234
235/// Default DoS limit for read lines
236pub const LIMIT_READ_LINES: usize = 100;
237/// Default DoS limit for read bytes per line
238pub const LIMIT_LINE_BYTES: u64 = 64_000;
239
240type Result<T> = ::std::result::Result<T, Ts3Error>;
241
242impl Drop for QueryClient {
243    fn drop(&mut self) {
244        #[allow(unused_variables)]
245        if let Err(e) = self.quit() {
246            #[cfg(feature = "debug_response")]
247            eprintln!("Can't quit on drop: {}", e);
248        }
249        let _ = self.tx.shutdown(Shutdown::Both);
250    }
251}
252
253impl QueryClient {
254    /// Create new query connection
255    pub fn new<A: ToSocketAddrs>(addr: A) -> Result<Self> {
256        let (rx, tx) = Self::new_inner(addr, None, None)?;
257
258        Ok(Self {
259            rx,
260            tx,
261            limit_lines: LIMIT_READ_LINES,
262            limit_lines_bytes: LIMIT_LINE_BYTES,
263        })
264    }
265
266    /// Create new query connection with timeouts
267    ///
268    /// `t_connect` is used for connection, `timeout` for read/write operations
269    pub fn with_timeout<A: ToSocketAddrs>(
270        addr: A,
271        t_connect: Option<Duration>,
272        timeout: Option<Duration>,
273    ) -> Result<Self> {
274        let (rx, tx) = Self::new_inner(addr, timeout, t_connect)?;
275
276        Ok(Self {
277            rx,
278            tx,
279            limit_lines: LIMIT_READ_LINES,
280            limit_lines_bytes: LIMIT_LINE_BYTES,
281        })
282    }
283
284    /// Set new maximum amount of lines to read per response, until DoS protection triggers.
285    pub fn limit_lines(&mut self, limit: usize) {
286        self.limit_lines = limit;
287    }
288
289    /// Set new maximum amount of bytes per line to read until DoS protection triggers.  
290    /// You may need to increase this for backup/restore of instances.
291    pub fn limit_line_bytes(&mut self, limit: u64) {
292        self.limit_lines_bytes = limit;
293    }
294
295    /// Rename this client, performs `clientupdate client_nickname` escaping the name
296    pub fn rename<T: AsRef<str>>(&mut self, name: T) -> Result<()> {
297        writeln!(
298            &mut self.tx,
299            "clientupdate client_nickname={}",
300            escape_arg(name)
301        )?;
302        let _ = self.read_response()?;
303        Ok(())
304    }
305
306    /// Update channel name, performs `channeledit channel_name`
307    pub fn rename_channel<T: AsRef<str>>(&mut self, channel: ChannelId, name: T) -> Result<()> {
308        writeln!(
309            &mut self.tx,
310            "channeledit cid={} channel_name={}",
311            channel,escape_arg(name)
312        )?;
313        let _ = self.read_response()?;
314        Ok(())
315    }
316
317    /// Update client description. If target is none updates this clients description.
318    ///
319    /// Performs `clientupdate CLIENT_DESCRIPTION` or `clientedit clid=` with `CLIENT_DESCRIPTION` if target is set.
320    pub fn update_description<T: AsRef<str>>(
321        &mut self,
322        descr: T,
323        target: Option<ClientId>,
324    ) -> Result<()> {
325        if let Some(clid) = target {
326            writeln!(
327                &mut self.tx,
328                "clientedit clid={} CLIENT_DESCRIPTION={}",
329                clid,
330                escape_arg(descr)
331            )?;
332        } else {
333            writeln!(
334                &mut self.tx,
335                "clientupdate CLIENT_DESCRIPTION={}",
336                escape_arg(descr)
337            )?;
338        }
339        let _ = self.read_response()?;
340        Ok(())
341    }
342
343    /// Poke a client.
344    ///
345    /// Performs `clientpoke`
346    pub fn poke_client<T: AsRef<str>>(&mut self, client: ClientId, msg: T) -> Result<()> {
347        writeln!(
348            &mut self.tx,
349            "clientpoke clid={} msg={}",
350            client,
351            msg.as_ref()
352        )?;
353        let _ = self.read_response()?;
354        Ok(())
355    }
356
357    /// Send chat message
358    pub fn send_message<T: AsRef<str>>(&mut self, target: MessageTarget, msg: T) -> Result<()> {
359        writeln!(
360            &mut self.tx,
361            "sendtextmessage {} msg={}",
362            target,
363            escape_arg(msg)
364        )?;
365        let _ = self.read_response()?;
366        Ok(())
367    }
368
369    /// Send quit command, does not close the socket, not to be exposed
370    fn quit(&mut self) -> Result<()> {
371        writeln!(&mut self.tx, "quit")?;
372        let _ = self.read_response()?;
373        Ok(())
374    }
375
376    /// Inner new-function that handles greeting etc
377    fn new_inner<A: ToSocketAddrs>(
378        addr: A,
379        timeout: Option<Duration>,
380        conn_timeout: Option<Duration>,
381    ) -> Result<(BufReader<TcpStream>, TcpStream)> {
382        let addr = addr
383            .to_socket_addrs()
384            .context(Io {
385                context: "invalid socket address",
386            })?
387            .next()
388            .context(InvalidSocketAddress {})?;
389        let stream = if let Some(dur) = conn_timeout {
390            TcpStream::connect_timeout(&addr, dur).context(Io {
391                context: "while connecting: ",
392            })?
393        } else {
394            TcpStream::connect(addr).context(Io {
395                context: "while connecting: ",
396            })?
397        };
398
399        stream.set_write_timeout(timeout).context(Io {
400            context: "setting write timeout: ",
401        })?;
402        stream.set_read_timeout(timeout).context(Io {
403            context: "setting read timeout: ",
404        })?;
405
406        stream.set_nodelay(true).context(Io {
407            context: "setting nodelay: ",
408        })?;
409
410        let mut reader = BufReader::new(stream.try_clone().context(Io {
411            context: "splitting connection: ",
412        })?);
413
414        // read server type token
415        let mut buffer = Vec::new();
416        reader.read_until(b'\r', &mut buffer).context(Io {
417            context: "reading response: ",
418        })?;
419
420        buffer.clear();
421        if let Err(e) = reader.read_until(b'\r', &mut buffer) {
422            use std::io::ErrorKind::*;
423            match e.kind() {
424                TimedOut | WouldBlock => (), // ignore no greeting
425                _ => return Err(e.into()),
426            }
427        }
428
429        Ok((reader, stream))
430    }
431
432    /// Perform a raw command, returns its response as raw value. (No unescaping is performed.)
433    ///
434    /// You need to escape the command properly.
435    pub fn raw_command<T: AsRef<str>>(&mut self, command: T) -> Result<Vec<String>> {
436        writeln!(&mut self.tx, "{}", command.as_ref())?;
437        let v = self.read_response()?;
438        Ok(v)
439    }
440
441    /// Performs `whoami`
442    ///
443    /// Returns a hashmap of entries. Values are unescaped if set.
444    pub fn whoami(&mut self, unescape: bool) -> Result<HashMap<String, Option<String>>> {
445        writeln!(&mut self.tx, "whoami")?;
446        let v = self.read_response()?;
447        Ok(parse_hashmap(v, unescape))
448    }
449
450    /// Logout
451    pub fn logout(&mut self) -> Result<()> {
452        writeln!(&mut self.tx, "logout")?;
453        let _ = self.read_response()?;
454        Ok(())
455    }
456
457    /// Login with provided data
458    ///
459    /// On drop queryclient issues a logout
460    pub fn login<T: AsRef<str>, S: AsRef<str>>(&mut self, user: T, password: S) -> Result<()> {
461        writeln!(
462            &mut self.tx,
463            "login {} {}",
464            escape_arg(user),
465            escape_arg(password)
466        )?;
467
468        let _ = self.read_response()?;
469
470        Ok(())
471    }
472
473    /// Select server to perform commands on, by port
474    ///
475    /// Performs `use port`
476    pub fn select_server_by_port(&mut self, port: u16) -> Result<()> {
477        writeln!(&mut self.tx, "use port={}", port)?;
478
479        let _ = self.read_response()?;
480        Ok(())
481    }
482
483    /// Move client to channel with optional channel password
484    ///
485    /// Performs `clientmove`
486    pub fn move_client(
487        &mut self,
488        client: ClientId,
489        channel: ChannelId,
490        password: Option<&str>,
491    ) -> Result<()> {
492        let pw_arg = if let Some(pw) = password {
493            format!("cpw={}", raw::escape_arg(pw).as_str())
494        } else {
495            String::new()
496        };
497        writeln!(
498            &mut self.tx,
499            "clientmove clid={} cid={} {}",
500            client, channel, pw_arg
501        )?;
502        let _ = self.read_response()?;
503        Ok(())
504    }
505
506    /// Kick client with specified message from channel/server. Message can't be longer than 40 characters.
507    ///
508    /// Performs `clientkick`
509    pub fn kick_client(
510        &mut self,
511        client: ClientId,
512        server: bool,
513        message: Option<&str>,
514    ) -> Result<()> {
515        let msg_arg = if let Some(pw) = message {
516            format!("reasonmsg={}", raw::escape_arg(pw).as_str())
517        } else {
518            String::new()
519        };
520        let rid = if server { 5 } else { 4 };
521        writeln!(
522            &mut self.tx,
523            "clientkick clid={} reasonid={} {}",
524            client, rid, msg_arg
525        )?;
526        let _ = self.read_response()?;
527        Ok(())
528    }
529
530    /// Create file directory in channel, has to be a valid path starting with `/`
531    ///
532    /// Performs `ftcreatedir`
533    pub fn create_dir<T: AsRef<str>>(&mut self, channel: ChannelId, path: T) -> Result<()> {
534        writeln!(
535            &mut self.tx,
536            "ftcreatedir cid={} cpw= dirname={}",
537            channel,
538            escape_arg(path)
539        )?;
540        let _ = self.read_response()?;
541        Ok(())
542    }
543
544    /// Delete File/Folder in channel, acts recursive on folders
545    ///
546    /// Example: `/My Directory` deletes everything inside that directory.
547    ///
548    /// Performs `ftdeletefile`
549    pub fn delete_file<T: AsRef<str>>(&mut self, channel: ChannelId, path: T) -> Result<()> {
550        writeln!(
551            &mut self.tx,
552            "ftdeletefile cid={} cpw= name={}",
553            channel,
554            escape_arg(path)
555        )?;
556        let _ = self.read_response()?;
557        Ok(())
558    }
559
560    /// Low-cost connection check
561    ///
562    /// Performs `whoami` command without parsing
563    pub fn ping(&mut self) -> Result<()> {
564        writeln!(&mut self.tx, "whoami")?;
565        let _ = self.read_response()?;
566        Ok(())
567    }
568
569    /// Select server to perform commands on, by server id.
570    ///
571    /// Performs `use sid`
572    pub fn select_server_by_id(&mut self, sid: ServerId) -> Result<()> {
573        writeln!(&mut self.tx, "use sid={}", sid)?;
574
575        let _ = self.read_response()?;
576        Ok(())
577    }
578
579    /// Performs `servergroupdelclient`  
580    /// Removes all client-db-ids in `cldbid` from the specified `group` id.
581    pub fn server_group_del_clients(
582        &mut self,
583        group: ServerGroupID,
584        cldbid: &[usize],
585    ) -> Result<()> {
586        if cldbid.is_empty() {
587            return Ok(());
588        }
589        writeln!(
590            &mut self.tx,
591            "servergroupdelclient sgid={} {}",
592            group,
593            Self::format_cldbids(cldbid)
594        )?;
595        let _ = self.read_response()?;
596        Ok(())
597    }
598
599    /// Performs `servergroupaddclient`  
600    /// Ads all specified `cldbid` clients to `group`.
601    pub fn server_group_add_clients(
602        &mut self,
603        group: ServerGroupID,
604        cldbid: &[usize],
605    ) -> Result<()> {
606        if cldbid.is_empty() {
607            return Ok(());
608        }
609        let v = Self::format_cldbids(cldbid);
610        writeln!(&mut self.tx, "servergroupaddclient sgid={} {}", group, v)?;
611        let _ = self.read_response()?;
612        Ok(())
613    }
614
615    /// Turn a list of client-db-ids into a list of cldbid=X
616    fn format_cldbids(it: &[usize]) -> String {
617        // would need itertools for format_with
618
619        let mut res = String::new();
620        let mut it = it.iter();
621        if let Some(n) = it.next() {
622            write!(res, "cldbid={}", n).unwrap();
623        }
624        for n in it {
625            write!(res, "|cldbid={}", n).unwrap();
626        }
627        res
628    }
629
630    /// Read response and check error line
631    fn read_response(&mut self) -> Result<Vec<String>> {
632        let mut result: Vec<String> = Vec::new();
633        let mut lr = (&mut self.rx).take(self.limit_lines_bytes);
634        for _ in 0..self.limit_lines {
635            let mut buffer = Vec::new();
636            // damn cargo fmt..
637            if lr.read_until(b'\r', &mut buffer).context(Io {
638                context: "reading response: ",
639            })? == 0
640            {
641                return ConnectionClosed {}.fail();
642            }
643            // we read until \r or max-read limit
644            if buffer.ends_with(&[b'\r']) {
645                buffer.pop();
646                if buffer.ends_with(&[b'\n']) {
647                    buffer.pop();
648                }
649            } else if lr.limit() == 0 {
650                return ResponseLimit { response: result }.fail();
651            } else {
652                return InvalidResponse {
653                    context: "expected \\r delimiter, got: ",
654                    data: String::from_utf8_lossy(&buffer),
655                }
656                .fail();
657            }
658
659            if !buffer.is_empty() {
660                let line = String::from_utf8(buffer).context(Utf8Error)?;
661                #[cfg(feature = "debug_response")]
662                println!("Read: {:?}", &line);
663                if line.starts_with("error ") {
664                    Self::check_ok(&line)?;
665                    return Ok(result);
666                }
667                result.push(line);
668            }
669            lr.set_limit(LIMIT_LINE_BYTES);
670        }
671        ResponseLimit { response: result }.fail()
672    }
673
674    /// Returns a list of online clients with full infos. Visiblity depends on current permissions. Values are unescaped where applicable.
675    ///
676    /// Performs `clientlist -uid -away -voice -times -groups -info -country -ip -badges`
677    pub fn online_clients_full(&mut self) -> Result<Vec<OnlineClientFull>> {
678        writeln!(
679            &mut self.tx,
680            "clientlist -uid -away -voice -times -groups -info -country -ip -badges"
681        )?;
682        let res = self.read_response()?;
683
684        let clients = raw::parse_multi_hashmap(res, false)
685            .into_iter()
686            .map(|v| Ok(OnlineClientFull::from_raw(v)?))
687            .collect::<Result<_>>()?;
688
689        Ok(clients)
690    }
691
692    /// Returns a list of online clients. Visiblity depends on current permissions. Values are unescaped where applicable.
693    ///
694    /// Performs `clientlist`
695    pub fn online_clients(&mut self) -> Result<Vec<OnlineClient>> {
696        writeln!(&mut self.tx, "clientlist")?;
697        let res = self.read_response()?;
698
699        let clients = raw::parse_multi_hashmap(res, false)
700            .into_iter()
701            .map(|v| Ok(OnlineClient::from_raw(v)?))
702            .collect::<Result<_>>()?;
703
704        Ok(clients)
705    }
706
707    /// Returns a list of channels. Values are unescaped where applicable.
708    ///
709    /// Performs `channellist`
710    pub fn channels(&mut self) -> Result<Vec<Channel>> {
711        writeln!(&mut self.tx, "channellist")?;
712        let res = self.read_response()?;
713
714        let channels = raw::parse_multi_hashmap(res, false)
715            .into_iter()
716            .map(|v| Ok(Channel::from_raw(v)?))
717            .collect::<Result<_>>()?;
718
719        Ok(channels)
720    }
721
722    /// Returns a list of channels with full infos. Values are unescaped where applicable.
723    ///
724    /// Performs `channellist -topic -flags -voice -limits -icon -secondsempty`
725    pub fn channels_full(&mut self) -> Result<Vec<ChannelFull>> {
726        writeln!(
727            &mut self.tx,
728            "channellist -topic -flags -voice -limits -icon -secondsempty"
729        )?;
730        let res = self.read_response()?;
731
732        let channels = raw::parse_multi_hashmap(res, false)
733            .into_iter()
734            .map(|v| Ok(ChannelFull::from_raw(v)?))
735            .collect::<Result<_>>()?;
736
737        Ok(channels)
738    }
739
740    /// Deletes a channel
741    ///
742    /// Performs `channeldelete cid={} force={}`
743    pub fn delete_channel(&mut self, id: ChannelId, force: bool) -> Result<()> {
744        writeln!(
745            &mut self.tx,
746            "channeldelete cid={} force={}",
747            id,
748            if force { 1 } else { 0 }
749        )?;
750        let _ = self.read_response()?;
751
752        Ok(())
753    }
754
755    /// Creates a channel
756    /// Performs `channelcreate`
757    pub fn create_channel(&mut self, channel: &ChannelEdit) -> Result<ChannelId> {
758        writeln!(&mut self.tx, "channelcreate{}", &channel.to_raw())?;
759        let res = self.read_response()?;
760
761        let mut response = raw::parse_hashmap(res, false);
762        let cid = int_val_parser(&mut response, "cid")?;
763
764        Ok(cid)
765    }
766
767    /// Returns a list of server groups. May contain templates and query groups if permitted. Values are unescaped where applicable.
768    ///
769    /// Performs `servergrouplist`
770    pub fn server_groups(&mut self) -> Result<Vec<ServerGroup>> {
771        writeln!(&mut self.tx, "servergrouplist")?;
772        let res = self.read_response()?;
773
774        let groups = raw::parse_multi_hashmap(res, false)
775            .into_iter()
776            .map(|v| Ok(ServerGroup::from_raw(v)?))
777            .collect::<Result<_>>()?;
778
779        Ok(groups)
780    }
781
782    /// Get a list of client-DB-IDs for a given server group ID
783    ///
784    /// See `servergroupclientlist`
785    pub fn servergroup_client_cldbids(&mut self, group: ServerGroupID) -> Result<Vec<usize>> {
786        writeln!(&mut self.tx, "servergroupclientlist sgid={}", group)?;
787
788        let resp = self.read_response()?;
789        if let Some(line) = resp.get(0) {
790            let data: Vec<usize> = line
791                .split('|')
792                .map(|e| {
793                    if let Some(cldbid) = e.split('=').collect::<Vec<_>>().get(1) {
794                        Ok(cldbid
795                            .parse::<usize>()
796                            .map_err(|_| Ts3Error::InvalidResponse {
797                                context: "expected usize, got ",
798                                data: line.to_string(),
799                            })?)
800                    } else {
801                        Err(Ts3Error::InvalidResponse {
802                            context: "expected data of cldbid=1, got ",
803                            data: line.to_string(),
804                        })
805                    }
806                })
807                .collect::<Result<Vec<usize>>>()?;
808            Ok(data)
809        } else {
810            Ok(Vec::new())
811        }
812    }
813
814    /// Check if error line is ok
815    fn check_ok(msg: &str) -> Result<()> {
816        let result: Vec<&str> = msg.split(' ').collect();
817        #[cfg(debug)]
818        {
819            // should only be invoked on `error` lines, sanity check
820            assert_eq!(
821                "check_ok invoked on non-error line",
822                result.get(0),
823                Some(&"error")
824            );
825        }
826        if let (Some(id), Some(msg)) = (result.get(1), result.get(2)) {
827            let split_id: Vec<&str> = id.split('=').collect();
828            let split_msg: Vec<&str> = msg.split('=').collect();
829            if let (Some(id), Some(msg)) = (split_id.get(1), split_msg.get(1)) {
830                let id = id.parse::<usize>().map_err(|_| Ts3Error::InvalidResponse {
831                    context: "expected usize, got ",
832                    data: (*msg).to_string(), // clippy lint
833                })?;
834                if id != 0 {
835                    return ServerError {
836                        response: ErrorResponse {
837                            id,
838                            msg: unescape_val(*msg),
839                        },
840                    }
841                    .fail();
842                } else {
843                    return Ok(());
844                }
845            }
846        }
847        Err(Ts3Error::InvalidResponse {
848            context: "expected id and msg, got ",
849            data: msg.to_string(),
850        })
851    }
852}
853
854#[cfg(test)]
855mod test {
856    use super::*;
857
858    #[test]
859    fn test_format_cldbids() {
860        let ids = vec![0, 1, 2, 3];
861        assert_eq!(
862            "cldbid=0|cldbid=1|cldbid=2|cldbid=3",
863            QueryClient::format_cldbids(&ids)
864        );
865        assert_eq!("", QueryClient::format_cldbids(&[]));
866        assert_eq!("cldbid=0", QueryClient::format_cldbids(&ids[0..1]));
867    }
868}