656 lines
21 KiB
Rust
656 lines
21 KiB
Rust
//! A simple IRC crate written in rust
|
|
//! ```no_run
|
|
//! use circe::{commands::Command, Client, Config};
|
|
//! fn main() -> Result<(), std::io::Error> {
|
|
//! let config = Default::default();
|
|
//! let mut client = Client::new(config)?;
|
|
//! client.identify()?;
|
|
//!
|
|
//! loop {
|
|
//! if let Ok(ref command) = client.read() {
|
|
//! if let Command::OTHER(line) = command {
|
|
//! print!("{}", line);
|
|
//! }
|
|
//! if let Command::PRIVMSG(nick, channel, message) = command {
|
|
//! println!("PRIVMSG received from {}: {} {}", nick, channel, message);
|
|
//! }
|
|
//! }
|
|
//! # break;
|
|
//! }
|
|
//! # Ok(())
|
|
//! }
|
|
|
|
#![warn(missing_docs)]
|
|
#![allow(clippy::too_many_lines)]
|
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
|
|
|
#[cfg(feature = "tls")]
|
|
use native_tls::TlsConnector;
|
|
|
|
use std::borrow::Cow;
|
|
use std::io::{Error, Read, Write};
|
|
use std::net::TcpStream;
|
|
|
|
#[cfg(feature = "toml_support")]
|
|
use serde_derive::Deserialize;
|
|
#[cfg(feature = "toml_support")]
|
|
use std::fs::File;
|
|
#[cfg(feature = "toml_support")]
|
|
use std::path::Path;
|
|
|
|
/// IRC comamnds
|
|
pub mod commands;
|
|
|
|
/// An IRC client
|
|
pub struct Client {
|
|
config: Config,
|
|
#[cfg(not(feature = "tls"))]
|
|
stream: TcpStream,
|
|
#[cfg(feature = "tls")]
|
|
stream: native_tls::TlsStream<TcpStream>,
|
|
}
|
|
|
|
/// Config for the IRC client
|
|
#[derive(Clone, Default)]
|
|
#[cfg_attr(feature = "toml_support", derive(Deserialize))]
|
|
pub struct Config {
|
|
channels: Vec<String>,
|
|
host: String,
|
|
mode: Option<String>,
|
|
nickname: Option<String>,
|
|
port: u16,
|
|
username: String,
|
|
}
|
|
|
|
/// Custom Error for the `read` function
|
|
#[derive(Debug)]
|
|
pub struct NoNewLines;
|
|
|
|
impl std::fmt::Display for NoNewLines {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "Now new lines from the stream.")
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for NoNewLines {}
|
|
|
|
impl Client {
|
|
/// Creates a new client with a given [`Config`].
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # let config = Default::default();
|
|
/// let mut client = Client::new(config)?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not connect to the host.
|
|
/// # Panics
|
|
/// Panics if the client can't connect to the given host.
|
|
pub fn new(config: Config) -> Result<Self, Error> {
|
|
let stream = TcpStream::connect(format!("{}:{}", config.host, config.port)).unwrap();
|
|
#[cfg(feature = "tls")]
|
|
let sslstream: native_tls::TlsStream<TcpStream>;
|
|
|
|
#[cfg(feature = "tls")]
|
|
{
|
|
let connector = TlsConnector::new().unwrap();
|
|
sslstream = connector.connect(config.host.as_str(), stream).unwrap();
|
|
};
|
|
|
|
#[cfg(feature = "tls")]
|
|
return Ok(Self {
|
|
config,
|
|
stream: sslstream,
|
|
});
|
|
|
|
#[cfg(not(feature = "tls"))]
|
|
return Ok(Self { config, stream });
|
|
}
|
|
|
|
/// Identify user and joins the in the [`Config`] specified channels.
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # let mut client = Client::new(Default::default())?;
|
|
/// client.identify()?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not write to the stream.
|
|
pub fn identify(&mut self) -> Result<(), Error> {
|
|
self.write_command(commands::Command::CAP(commands::CapMode::LS))?;
|
|
self.write_command(commands::Command::CAP(commands::CapMode::END))?;
|
|
|
|
self.write_command(commands::Command::USER(
|
|
self.config.username.clone(),
|
|
"*".into(),
|
|
"*".into(),
|
|
self.config.username.clone(),
|
|
))?;
|
|
|
|
if let Some(nick) = self.config.nickname.clone() {
|
|
self.write_command(commands::Command::NICK(nick))?;
|
|
} else {
|
|
self.write_command(commands::Command::NICK(self.config.username.clone()))?;
|
|
}
|
|
|
|
loop {
|
|
if let Ok(ref command) = self.read() {
|
|
match command {
|
|
commands::Command::PING(code) => {
|
|
self.write_command(commands::Command::PONG(code.to_string()))?;
|
|
}
|
|
commands::Command::OTHER(line) => {
|
|
#[cfg(feature = "debug")]
|
|
println!("{}", line);
|
|
if line.contains("001") {
|
|
break;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
let config = self.config.clone();
|
|
self.write_command(commands::Command::MODE(config.username, config.mode))?;
|
|
for channel in &config.channels {
|
|
self.write_command(commands::Command::JOIN(channel.to_string()))?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn read_string(&mut self) -> Option<String> {
|
|
let mut buffer = [0u8; 512];
|
|
|
|
let num_bytes = match self.stream.read(&mut buffer) {
|
|
Ok(b) => b,
|
|
Err(_) => 0,
|
|
};
|
|
|
|
if num_bytes == 0 {
|
|
return None;
|
|
}
|
|
|
|
// Slice to the number of bytes that were actually read
|
|
let res = String::from_utf8_lossy(&buffer[..num_bytes]);
|
|
|
|
Some(res.into())
|
|
}
|
|
|
|
/// Read data coming from the IRC as a [`commands::Command`].
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # use circe::commands::Command;
|
|
/// # fn main() -> Result<(), std::io::Error> {
|
|
/// # let config = Default::default();
|
|
/// # let mut client = Client::new(config)?;
|
|
/// if let Ok(ref command) = client.read() {
|
|
/// if let Command::OTHER(line) = command {
|
|
/// print!("{}", line);
|
|
/// }
|
|
/// }
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// # }
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if there are no new messages. This should not be taken as an actual error, because nothing went wrong.
|
|
pub fn read(&mut self) -> Result<commands::Command, NoNewLines> {
|
|
if let Some(string) = self.read_string() {
|
|
#[cfg(feature = "debug")]
|
|
println!("{:#?}", string);
|
|
|
|
let command = commands::Command::command_from_str(&string);
|
|
|
|
if let commands::Command::PONG(command) = command {
|
|
if let Err(_e) = self.write_command(commands::Command::PONG(command)) {
|
|
return Err(NoNewLines);
|
|
}
|
|
return Ok(commands::Command::PONG("".to_string()));
|
|
}
|
|
|
|
return Ok(command);
|
|
}
|
|
|
|
Err(NoNewLines)
|
|
}
|
|
|
|
fn write(&mut self, data: &str) -> Result<(), Error> {
|
|
let formatted = {
|
|
let new = format!("{}\r\n", data);
|
|
Cow::Owned(new) as Cow<str>
|
|
};
|
|
|
|
#[cfg(feature = "debug")]
|
|
println!("{:?}", formatted);
|
|
|
|
self.stream.write_all(formatted.as_bytes())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Send a [`commands::Command`] to the IRC.<br>
|
|
/// Not reccomended to use, use the helper functions instead.
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # use circe::commands::Command;
|
|
/// # let mut client = Client::new(Default::default())?;
|
|
/// client.write_command(Command::PRIVMSG("".to_string(), "#main".to_string(), "Hello".to_string()))?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not write to the stream.
|
|
pub fn write_command(&mut self, command: commands::Command) -> Result<(), Error> {
|
|
use commands::Command::{
|
|
ADMIN, AWAY, CAP, INVITE, JOIN, LIST, MODE, NAMES, NICK, OPER, OTHER, PART, PASS, PING,
|
|
PONG, PRIVMSG, QUIT, TOPIC, USER,
|
|
};
|
|
let computed = match command {
|
|
ADMIN(target) => {
|
|
let formatted = format!("ADMIN {}", target);
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
AWAY(message) => {
|
|
let formatted = format!("AWAY {}", message);
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
CAP(mode) => {
|
|
use commands::CapMode::{END, LS};
|
|
Cow::Borrowed(match mode {
|
|
LS => "CAP LS 302",
|
|
END => "CAP END",
|
|
}) as Cow<str>
|
|
}
|
|
INVITE(username, channel) => {
|
|
let formatted = format!("INVITE {} {}", username, channel);
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
JOIN(channel) => {
|
|
let formatted = format!("JOIN {}", channel);
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
LIST(channel, server) => {
|
|
let mut formatted = "LIST".to_string();
|
|
if let Some(channel) = channel {
|
|
formatted.push_str(format!(" {}", channel).as_str());
|
|
}
|
|
if let Some(server) = server {
|
|
formatted.push_str(format!(" {}", server).as_str());
|
|
}
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
NAMES(channel, server) => {
|
|
let formatted = {
|
|
if let Some(server) = server {
|
|
format!("NAMES {} {}", channel, server)
|
|
} else {
|
|
format!("NAMES {}", channel)
|
|
}
|
|
};
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
NICK(nickname) => {
|
|
let formatted = format!("NICK {}", nickname);
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
MODE(target, mode) => {
|
|
let formatted = {
|
|
if let Some(mode) = mode {
|
|
format!("MODE {} {}", target, mode)
|
|
} else {
|
|
format!("MODE {}", target)
|
|
}
|
|
};
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
OPER(nick, password) => {
|
|
let formatted = format!("OPER {} {}", nick, password);
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
OTHER(_) => {
|
|
return Err(Error::new(
|
|
std::io::ErrorKind::Other,
|
|
"Cannot write commands of type OTHER",
|
|
));
|
|
}
|
|
PART(target) => {
|
|
let formatted = format!("PART {}", target);
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
PASS(password) => {
|
|
let formatted = format!("PASS {}", password);
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
PING(target) => {
|
|
let formatted = format!("PING {}", target);
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
PONG(code) => {
|
|
let formatted = format!("PONG {}", code);
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
PRIVMSG(_, target, message) => {
|
|
let formatted = format!("PRIVMSG {} {}", target, message);
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
QUIT(message) => {
|
|
let formatted = format!("QUIT :{}", message);
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
TOPIC(channel, topic) => {
|
|
let formatted = {
|
|
if let Some(topic) = topic {
|
|
format!("TOPIC {} :{}", channel, topic)
|
|
} else {
|
|
format!("TOPIC {}", channel)
|
|
}
|
|
};
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
USER(username, s1, s2, realname) => {
|
|
let formatted = format!("USER {} {} {} :{}", username, s1, s2, realname);
|
|
Cow::Owned(formatted) as Cow<str>
|
|
}
|
|
};
|
|
|
|
self.write(&computed)?;
|
|
Ok(())
|
|
}
|
|
|
|
// Helper commands
|
|
|
|
/// Helper function for requesting information about the ADMIN of an IRC server
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # let mut client = Client::new(Default::default())?;
|
|
/// client.admin("192.168.178.100")?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not write to the stream.
|
|
pub fn admin(&mut self, target: &str) -> Result<(), Error> {
|
|
self.write_command(commands::Command::ADMIN(target.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function for setting the users status to AWAY
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # let mut client = Client::new(Default::default())?;
|
|
/// client.away("AFK")?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not write to the stream.
|
|
pub fn away(&mut self, message: &str) -> Result<(), Error> {
|
|
self.write_command(commands::Command::AWAY(message.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function for sending PRIVMSGs.
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # let mut client = Client::new(Default::default())?;
|
|
/// client.privmsg("#main", "Hello")?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not write to the stream.
|
|
pub fn privmsg(&mut self, channel: &str, message: &str) -> Result<(), Error> {
|
|
self.write_command(commands::Command::PRIVMSG(
|
|
String::from(""),
|
|
channel.to_string(),
|
|
message.to_string(),
|
|
))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function to INVITE people to a channels
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # let mut client = Client::new(Default::default())?;
|
|
/// client.invite("liblirc", "#circe")?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not write to the stream.
|
|
pub fn invite(&mut self, username: &str, channel: &str) -> Result<(), Error> {
|
|
self.write_command(commands::Command::INVITE(
|
|
username.to_string(),
|
|
channel.to_string(),
|
|
))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function for sending JOINs.
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # let mut client = Client::new(Default::default())?;
|
|
/// client.join("#main")?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not write to the stream.
|
|
pub fn join(&mut self, channel: &str) -> Result<(), Error> {
|
|
self.write_command(commands::Command::JOIN(channel.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function for ``LISTing`` channels and modes
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # let mut client = Client::new(Default::default())?;
|
|
/// client.list(None, None)?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not write to the stream.
|
|
pub fn list(&mut self, channel: Option<&str>, server: Option<&str>) -> Result<(), Error> {
|
|
let channel_config = channel.map(std::string::ToString::to_string);
|
|
let server_config = server.map(std::string::ToString::to_string);
|
|
self.write_command(commands::Command::LIST(channel_config, server_config))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function for getting all nicknames in a channel
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # let mut client = Client::new(Default::default())?;
|
|
/// client.names("#main,#circe", None)?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not write to the stream.
|
|
pub fn names(&mut self, channel: &str, server: Option<&str>) -> Result<(), Error> {
|
|
if let Some(server) = server {
|
|
self.write_command(commands::Command::NAMES(
|
|
channel.to_string(),
|
|
Some(server.to_string()),
|
|
))?;
|
|
} else {
|
|
self.write_command(commands::Command::NAMES(channel.to_string(), None))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function to try to register as a channel operator
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # let mut client = Client::new(Default::default())?;
|
|
/// client.oper("username", "password")?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not write to the stream.
|
|
pub fn oper(&mut self, username: &str, password: &str) -> Result<(), Error> {
|
|
self.write_command(commands::Command::OPER(
|
|
username.to_string(),
|
|
password.to_string(),
|
|
))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function for sending MODEs.
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # let mut client = Client::new(Default::default())?;
|
|
/// client.mode("test", Some("+B"))?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not write to the stream.
|
|
pub fn mode(&mut self, target: &str, mode: Option<&str>) -> Result<(), Error> {
|
|
if let Some(mode) = mode {
|
|
self.write_command(commands::Command::MODE(
|
|
target.to_string(),
|
|
Some(mode.to_string()),
|
|
))?;
|
|
} else {
|
|
self.write_command(commands::Command::MODE(target.to_string(), None))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function for leaving channels.
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # let mut client = Client::new(Default::default())?;
|
|
/// client.part("#main")?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not write to the stream.
|
|
pub fn part(&mut self, target: &str) -> Result<(), Error> {
|
|
self.write_command(commands::Command::PART(target.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function for setting or getting the topic of a channel
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # let mut client = Client::new(Default::default())?;
|
|
/// client.topic("#main", Some("main channel"))?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not write to the stream.
|
|
pub fn topic(&mut self, channel: &str, topic: Option<&str>) -> Result<(), Error> {
|
|
if let Some(topic) = topic {
|
|
self.write_command(commands::Command::TOPIC(
|
|
channel.to_string(),
|
|
Some(topic.to_string()),
|
|
))?;
|
|
} else {
|
|
self.write_command(commands::Command::TOPIC(channel.to_string(), None))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function for leaving the IRC server and shutting down the TCP stream afterwards.
|
|
/// ```no_run
|
|
/// # use circe::*;
|
|
/// # let mut client = Client::new(Default::default())?;
|
|
/// client.quit(None)?;
|
|
/// # Ok::<(), std::io::Error>(())
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns error if the client could not write to the stream.
|
|
pub fn quit(&mut self, message: Option<&str>) -> Result<(), Error> {
|
|
if let Some(message) = message {
|
|
self.write_command(commands::Command::QUIT(message.to_string()))?;
|
|
} else {
|
|
self.write_command(commands::Command::QUIT(format!(
|
|
"circe {} (https://crates.io/crates/circe)",
|
|
env!("CARGO_PKG_VERSION")
|
|
)))?;
|
|
}
|
|
|
|
#[cfg(not(feature = "tls"))]
|
|
self.stream.shutdown(std::net::Shutdown::Both)?;
|
|
|
|
#[cfg(feature = "tls")]
|
|
self.stream.shutdown()?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Config {
|
|
/// Create a new config for the client
|
|
///
|
|
/// ```rust
|
|
/// # use circe::*;
|
|
/// let config = Config::new(
|
|
/// &["#main", "#circe"],
|
|
/// "192.168.178.100",
|
|
/// Some("+B"),
|
|
/// Some("circe"),
|
|
/// 6667,
|
|
/// "circe",
|
|
/// );
|
|
/// ```
|
|
#[must_use]
|
|
pub fn new(
|
|
channels: &[&'static str],
|
|
host: &str,
|
|
mode: Option<&'static str>,
|
|
nickname: Option<&'static str>,
|
|
port: u16,
|
|
username: &str,
|
|
) -> Self {
|
|
// Conversion from &'static str to String
|
|
let channels_config = channels
|
|
.iter()
|
|
.map(|channel| (*channel).to_string())
|
|
.collect();
|
|
|
|
let mode_config: Option<String>;
|
|
if let Some(mode) = mode {
|
|
mode_config = Some(mode.to_string());
|
|
} else {
|
|
mode_config = None;
|
|
}
|
|
|
|
let nickname_config: Option<String>;
|
|
if let Some(nickname) = nickname {
|
|
nickname_config = Some(nickname.to_string());
|
|
} else {
|
|
nickname_config = Some(username.to_string());
|
|
}
|
|
|
|
Self {
|
|
channels: channels_config,
|
|
host: host.into(),
|
|
mode: mode_config,
|
|
nickname: nickname_config,
|
|
port,
|
|
username: username.into(),
|
|
}
|
|
}
|
|
|
|
/// Create a config from a toml file
|
|
/// ```toml
|
|
/// channels = ["#main", "#main2"]
|
|
/// host = "192.168.178.100"
|
|
/// mode = "+B"
|
|
/// nickname = "circe"
|
|
/// port = 6667
|
|
/// username = "circe"
|
|
/// ```
|
|
/// # Errors
|
|
/// Returns an Error if the file cannot be opened or if the TOML is invalid
|
|
///
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "toml_support")))]
|
|
#[cfg(feature = "toml_support")]
|
|
pub fn from_toml<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
|
|
let mut file = File::open(&path)?;
|
|
let mut data = String::new();
|
|
file.read_to_string(&mut data)?;
|
|
|
|
toml::from_str(&data).map_err(|e| {
|
|
use std::io::ErrorKind;
|
|
Error::new(ErrorKind::Other, format!("Invalid TOML: {}", e))
|
|
})
|
|
}
|
|
}
|