things
This commit is contained in:
parent
6e4d92a030
commit
dc47ec918b
16 changed files with 600 additions and 5 deletions
36
Cargo.lock
generated
36
Cargo.lock
generated
|
@ -14,6 +14,8 @@ dependencies = [
|
|||
"matrix-sdk",
|
||||
"serde",
|
||||
"serenity",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tokio",
|
||||
"toml 0.7.4",
|
||||
]
|
||||
|
@ -1010,6 +1012,12 @@ dependencies = [
|
|||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.2.6"
|
||||
|
@ -2382,6 +2390,12 @@ dependencies = [
|
|||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.13"
|
||||
|
@ -2650,6 +2664,28 @@ version = "0.10.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.24.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2 1.0.59",
|
||||
"quote 1.0.27",
|
||||
"rustversion",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
|
|
|
@ -8,6 +8,7 @@ readme = "README.md"
|
|||
[features]
|
||||
# platforms
|
||||
irc = ["dep:irc"]
|
||||
irc-sp = ["tokio/net"]
|
||||
matrix = ["dep:matrix-sdk"]
|
||||
discord = ["dep:serenity"]
|
||||
# tls
|
||||
|
@ -16,7 +17,7 @@ tls-rustls = ["irc?/tls-rust", "matrix-sdk?/rustls-tls", "serenity?/rustls_bac
|
|||
# extra
|
||||
matrix-e2ee = ["matrix", "matrix-sdk?/e2e-encryption"]
|
||||
# default
|
||||
default = ["tls-rustls", "irc", "discord", "matrix", "matrix-e2ee"]
|
||||
default = ["tls-rustls", "irc", "irc-sp", "discord", "matrix", "matrix-e2ee"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
|
@ -26,6 +27,8 @@ log = "0.4"
|
|||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
tokio = { version = "1.28", features = ["rt-multi-thread", "macros", "time", "sync"] }
|
||||
toml = "0.7"
|
||||
strum_macros = "0.24"
|
||||
strum = { version = "0.24", features = ["derive"] }
|
||||
|
||||
[dependencies.irc]
|
||||
optional = true
|
||||
|
|
|
@ -21,6 +21,7 @@ impl Task for DiscordTask {
|
|||
async fn start(&self, id: Id, tx: Sender, mut rx: Receiver) -> TaskResult {
|
||||
let handler = Handler {
|
||||
links: self.links.clone(),
|
||||
suffix: self.config.suffix.clone(),
|
||||
tx,
|
||||
id,
|
||||
};
|
||||
|
@ -64,7 +65,7 @@ impl DiscordTask {
|
|||
hook.execute(http, false, |hook| {
|
||||
hook.username(&msg.author)
|
||||
.content(&msg.content)
|
||||
.avatar_url(msg.avatar.as_ref().map(|s| s.as_str()).unwrap_or(""))
|
||||
.avatar_url(msg.avatar.as_deref().unwrap_or(""))
|
||||
}).await?;
|
||||
} else {
|
||||
channel.say(http, &content).await?;
|
||||
|
@ -76,6 +77,7 @@ impl DiscordTask {
|
|||
|
||||
struct Handler {
|
||||
id: Id,
|
||||
suffix: Arc<str>,
|
||||
links: Arc<Linkmap<ChannelId>>,
|
||||
tx: Sender,
|
||||
}
|
||||
|
@ -98,6 +100,7 @@ impl EventHandler for Handler {
|
|||
origin: (self.id, msg.channel_id.to_string()),
|
||||
link: link.clone(),
|
||||
author,
|
||||
suffix: self.suffix.clone(),
|
||||
content: msg.content,
|
||||
avatar,
|
||||
}).expect("failed to broadcast message");
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use serde::Deserialize;
|
||||
use serenity::model::prelude::{WebhookId, ChannelId};
|
||||
|
||||
fn default_suffix() -> Arc<str> {
|
||||
"[d]".into()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DiscordConfig {
|
||||
pub token: String,
|
||||
#[serde(default)]
|
||||
pub webhooks: HashMap<ChannelId, WebhookId>,
|
||||
#[serde(default="default_suffix")]
|
||||
pub suffix: Arc<str>,
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ impl IrcTask {
|
|||
origin: (id, channel.clone()),
|
||||
link: link.clone(),
|
||||
author: author.to_owned(),
|
||||
suffix: self.config.suffix.clone(),
|
||||
content: message.clone(),
|
||||
avatar: None,
|
||||
})?;
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
const fn default_port() -> u16 { 6667 }
|
||||
const fn default_tls() -> bool { true }
|
||||
fn default_suffix() -> Arc<str> {
|
||||
"[i]".into()
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct IrcConfig {
|
||||
|
@ -13,4 +19,6 @@ pub struct IrcConfig {
|
|||
pub nick: String,
|
||||
#[serde(default)]
|
||||
pub alt_nicks: Vec<String>,
|
||||
#[serde(default="default_suffix")]
|
||||
pub suffix: Arc<str>,
|
||||
}
|
||||
|
|
192
src/bridge_irc_sp/bridge.rs
Normal file
192
src/bridge_irc_sp/bridge.rs
Normal file
|
@ -0,0 +1,192 @@
|
|||
use std::collections::{HashSet, HashMap};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, warn};
|
||||
use serde::Deserialize;
|
||||
use tokio::{net::TcpStream, io::{BufReader, BufWriter, AsyncWrite}, select};
|
||||
|
||||
use crate::{supervisor::{Id, TaskResult, Sender, Task, Receiver}, linkmap::Linkmap};
|
||||
use crate::supervisor::Message as BMessage;
|
||||
|
||||
use super::{IrcSpConfig, message::{Message, Command}, network::{MessageReader, MessageWriter}};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IrcSpTask {
|
||||
config: IrcSpConfig,
|
||||
links: Linkmap<String>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Task for IrcSpTask {
|
||||
async fn start(&self, id: Id, tx: Sender, mut rx: Receiver) -> TaskResult {
|
||||
let stream = TcpStream::connect((self.config.peer_host.as_ref(), self.config.peer_port)).await?;
|
||||
let (reader, writer) = stream.into_split();
|
||||
let mut reader = MessageReader::new(BufReader::new(reader));
|
||||
|
||||
let mut ctx = Context {
|
||||
id,
|
||||
links: &self.links,
|
||||
cfg: &self.config,
|
||||
writer: MessageWriter::new(BufWriter::new(writer)),
|
||||
tx,
|
||||
servers: HashSet::from([self.config.server_name.clone()]),
|
||||
nicks: HashSet::from([self.config.user_nick.clone()]),
|
||||
local_users: HashSet::from([self.config.user_nick.clone()]),
|
||||
};
|
||||
|
||||
|
||||
debug!("{id}: identifying");
|
||||
|
||||
ctx.writer.write_msg(Message::new(Command::PASS, vec![&ctx.cfg.peer_password, "0210", "abridged|"])).await?;
|
||||
ctx.writer.write_msg(Message::new(Command::SERVER, vec![&ctx.cfg.server_name, ":Abridged"])).await?;
|
||||
ctx.writer.flush().await?;
|
||||
|
||||
debug!("{id}: checking peer identification");
|
||||
|
||||
let passmsg = reader.read_message().await?;
|
||||
|
||||
if passmsg.command != Command::PASS {
|
||||
return Err("Invalid handshake, expected PASS".into())
|
||||
}
|
||||
|
||||
if passmsg.params.first() != Some(&ctx.cfg.password.as_str()) {
|
||||
return Err("Configured local password does not match password from peer".into())
|
||||
}
|
||||
|
||||
ctx.writer.write_msg(Message::with_prefix(
|
||||
&ctx.cfg.server_name,
|
||||
Command::NICK,
|
||||
vec![&ctx.cfg.user_nick, "0", &ctx.cfg.user_nick, "server", "1", "+", &ctx.cfg.user_nick]
|
||||
)).await?;
|
||||
|
||||
for channel in self.links.iter_channels() {
|
||||
ctx.writer.write_msg(Message::with_prefix(
|
||||
&ctx.cfg.user_nick,
|
||||
Command::JOIN,
|
||||
vec![channel],
|
||||
)).await?;
|
||||
}
|
||||
ctx.writer.flush().await?;
|
||||
|
||||
debug!("{id}: entering event loop");
|
||||
loop { select! {
|
||||
msg = reader.read_message() => handle_irc_msg(msg?, &mut ctx).await?,
|
||||
msg = rx.recv() => handle_bridge_msg(msg?, &mut ctx).await?,
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
struct Context<'a, W: AsyncWrite + Unpin> {
|
||||
id: Id,
|
||||
links: &'a Linkmap<String>,
|
||||
cfg: &'a IrcSpConfig,
|
||||
writer: MessageWriter<W>,
|
||||
tx: Sender,
|
||||
servers: HashSet<String>,
|
||||
nicks: HashSet<String>,
|
||||
local_users: HashMap<(String, &str), String>,
|
||||
}
|
||||
|
||||
const MALFORMED: &str = "Malformed IRC message";
|
||||
|
||||
async fn handle_bridge_msg(msg: BMessage, ctx: &mut Context<'_, impl AsyncWrite + Unpin>) -> TaskResult {
|
||||
let key = &(msg.author, msg.suffix.as_ref());
|
||||
let nick = match ctx.local_users.get(&key) {
|
||||
Some(nick) => nick,
|
||||
None => {
|
||||
let nick = msg.author + msg.suffix.as_ref();
|
||||
ctx.writer.write_msg(Message::with_prefix(
|
||||
&ctx.cfg.server_name,
|
||||
Command::NICK,
|
||||
vec![&nick, "0", &nick, "server", "1", "+", &nick]
|
||||
)).await?;
|
||||
ctx.local_users.insert(todo!(), nick.clone());
|
||||
ctx.nicks.insert(nick.clone());
|
||||
&nick
|
||||
}
|
||||
};
|
||||
for channel in ctx.links.get_channels(&msg.link) {
|
||||
if msg.origin.0 == ctx.id && &msg.origin.1 == channel {
|
||||
continue
|
||||
}
|
||||
ctx.writer.write_msg(Message::with_prefix(
|
||||
&nick,
|
||||
Command::PRIVMSG,
|
||||
vec![channel, &msg.content]
|
||||
)).await?;
|
||||
ctx.writer.flush().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_irc_msg<'a>(msg: Message<'a>, ctx: &mut Context<'_, impl AsyncWrite + Unpin>) -> TaskResult {
|
||||
debug!("irc message: {:?}", msg);
|
||||
match msg.command {
|
||||
Command::PASS => todo!(),
|
||||
|
||||
Command::SERVER => {
|
||||
let name = *msg.params.first().ok_or(MALFORMED)?;
|
||||
debug!("linked server {}", name);
|
||||
ctx.servers.insert(name.to_owned());
|
||||
}
|
||||
|
||||
Command::NICK => {
|
||||
let prefix = msg.prefix.ok_or(MALFORMED)?;
|
||||
let name = *msg.params.first().ok_or(MALFORMED)?;
|
||||
if ctx.nicks.contains(prefix) {
|
||||
ctx.nicks.remove(prefix);
|
||||
debug!("updated nick for {} to {}", prefix, name);
|
||||
} else {
|
||||
debug!("connecting user {}", name);
|
||||
}
|
||||
ctx.nicks.insert(name.to_owned());
|
||||
},
|
||||
|
||||
Command::PING => {
|
||||
let prefix = msg.prefix.ok_or(MALFORMED)?;
|
||||
let params = match msg.params.first() {
|
||||
Some(m) => vec![prefix, m],
|
||||
None => vec![prefix],
|
||||
};
|
||||
ctx.writer.write_msg(Message::with_prefix(&ctx.cfg.server_name, Command::PONG, params)).await?;
|
||||
ctx.writer.flush().await?;
|
||||
}
|
||||
|
||||
Command::PRIVMSG => {
|
||||
let channel = msg.params.first().ok_or(MALFORMED)?.to_string();
|
||||
if let Some(link) = ctx.links.get_link(&channel) {
|
||||
let author = msg.prefix.ok_or(MALFORMED)?.to_owned();
|
||||
let content = msg.params.get(1).ok_or(MALFORMED)?.to_string();
|
||||
ctx.tx.send(BMessage {
|
||||
origin: (ctx.id, channel.clone()),
|
||||
link: link.clone(),
|
||||
author,
|
||||
suffix: ctx.cfg.suffix.clone(),
|
||||
content,
|
||||
avatar: None,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Command::ERROR => {
|
||||
warn!("Error: {:?}", msg.params.first());
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/*
|
||||
+ PASS A_pitowoVsjRgZ 0210 proxy|
|
||||
+ SERVER c.trimill.xyz :Server Info Text
|
||||
- :trimill.xyz PASS C_kTNGh3SlfpJ5 0210 proxy|
|
||||
- :trimill.xyz SERVER trimill.xyz 1 :Server Info Text
|
||||
- :trimill.xyz NICK trimill 1 trimill server 1 + :trimill
|
||||
- :trimill.xyz NJOIN #test2 :@trimill
|
||||
- :trimill.xyz NJOIN #test :@trimill
|
||||
- :trimill.xyz PING :trimill.xyz
|
||||
+ :c.trimill.xyz PING :c.trimill.xyz
|
||||
+ :c.trimill.xyz PONG trimill.xyz :trimill.xyz
|
||||
- :trimill.xyz PONG c.trimill.xyz :c.trimill.xyz
|
||||
*/
|
33
src/bridge_irc_sp/config.rs
Normal file
33
src/bridge_irc_sp/config.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
const fn default_port() -> u16 { 6667 }
|
||||
const fn default_nick_len() -> u32 { 9 }
|
||||
fn default_user_nick() -> String {
|
||||
"abridged".to_owned()
|
||||
}
|
||||
fn default_suffix() -> Arc<str> {
|
||||
"[i]".into()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct IrcSpConfig {
|
||||
pub server_name: String,
|
||||
|
||||
pub peer_host: String,
|
||||
#[serde(default="default_port")]
|
||||
pub peer_port: u16,
|
||||
|
||||
pub password: String,
|
||||
pub peer_password: String,
|
||||
|
||||
#[serde(default="default_user_nick")]
|
||||
pub user_nick: String,
|
||||
|
||||
#[serde(default="default_nick_len")]
|
||||
pub max_nick_len: u32,
|
||||
|
||||
#[serde(default="default_suffix")]
|
||||
pub suffix: Arc<str>,
|
||||
}
|
217
src/bridge_irc_sp/message.rs
Normal file
217
src/bridge_irc_sp/message.rs
Normal file
|
@ -0,0 +1,217 @@
|
|||
use std::{str::{Chars, FromStr}, iter::Peekable, ops::Range, fmt};
|
||||
|
||||
use strum_macros::{EnumString, Display};
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumString, Display)]
|
||||
pub enum Command {
|
||||
PASS,
|
||||
SERVER,
|
||||
NICK,
|
||||
NJOIN,
|
||||
PING,
|
||||
PONG,
|
||||
MODE,
|
||||
JOIN,
|
||||
PRIVMSG,
|
||||
ERROR,
|
||||
#[strum(disabled)]
|
||||
Number(u16),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Message<'a> {
|
||||
pub prefix: Option<&'a str>,
|
||||
pub command: Command,
|
||||
pub params: Vec<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> Message<'a> {
|
||||
pub fn new(command: Command, params: Vec<&'a str>) -> Self {
|
||||
Self {
|
||||
prefix: None,
|
||||
command,
|
||||
params,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_prefix(prefix: &'a str, command: Command, params: Vec<&'a str>) -> Self {
|
||||
Self {
|
||||
prefix: Some(prefix),
|
||||
command,
|
||||
params,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for Message<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(prefix) = self.prefix {
|
||||
write!(f, ":{} ", prefix)?;
|
||||
}
|
||||
write!(f, "{}", self.command)?;
|
||||
let paramc = self.params.len();
|
||||
if paramc > 0 {
|
||||
for param in &self.params[..(paramc-1)] {
|
||||
write!(f, " {}", param)?;
|
||||
}
|
||||
write!(f, " :{}", &self.params[paramc-1])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseError {
|
||||
msg: String,
|
||||
span: Range<usize>,
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}: {}", self.span, self.msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ParseError {}
|
||||
|
||||
struct Parser<'a> {
|
||||
src: &'a str,
|
||||
chars: Peekable<Chars<'a>>,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
fn next(&mut self) -> Option<char> {
|
||||
self.chars
|
||||
.next()
|
||||
.map(|c| {
|
||||
self.pos += 1;
|
||||
c
|
||||
})
|
||||
}
|
||||
|
||||
fn peek(&mut self) -> Option<char> {
|
||||
self.chars.peek().copied()
|
||||
}
|
||||
|
||||
fn read_word(&mut self) -> &'a str {
|
||||
let start = self.pos;
|
||||
while !matches!(self.peek(), Some(' ') | Some('\n') | Some('\r') | None) {
|
||||
self.next();
|
||||
}
|
||||
&self.src[start..self.pos]
|
||||
}
|
||||
|
||||
fn parse(&mut self) -> Result<Message<'a>, ParseError> {
|
||||
let prefix = if let Some(':') = self.peek() {
|
||||
self.next();
|
||||
let p = self.read_word();
|
||||
match self.peek() {
|
||||
Some(' ') => { self.next(); },
|
||||
Some(c) => return Err(ParseError {
|
||||
msg: format!("expected space, found '{c}'"),
|
||||
span: self.pos..(self.pos+1),
|
||||
}),
|
||||
None => return Err(ParseError {
|
||||
msg: "expected space, found EOF".to_owned(),
|
||||
span: self.pos..(self.pos+1)
|
||||
})
|
||||
}
|
||||
Some(p)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let command = self.parse_command()?;
|
||||
let mut params = Vec::new();
|
||||
while self.peek().is_some() {
|
||||
params.push(self.parse_param()?);
|
||||
}
|
||||
Ok(Message {
|
||||
prefix,
|
||||
command,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_param(&mut self) -> Result<&'a str, ParseError> {
|
||||
let Some(' ') = self.peek() else {
|
||||
let pos = self.pos;
|
||||
return Err(ParseError {
|
||||
msg: format!("expected space, found '{}'", self.next().unwrap()),
|
||||
span: pos..(pos+1),
|
||||
})
|
||||
};
|
||||
self.next();
|
||||
if let Some(':') = self.peek() {
|
||||
self.next();
|
||||
let start = self.pos;
|
||||
while self.next().is_some() {}
|
||||
Ok(&self.src[start..])
|
||||
} else {
|
||||
let start = self.pos;
|
||||
while !matches!(self.peek(), Some(' ') | Some('\n') | Some('\r') | None) {
|
||||
self.next();
|
||||
}
|
||||
Ok(&self.src[start..self.pos])
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_command(&mut self) -> Result<Command, ParseError> {
|
||||
let start = self.pos;
|
||||
let name = self.read_word();
|
||||
if name.len() == 3 {
|
||||
if let Ok(n) = name.parse::<u16>() {
|
||||
return Ok(Command::Number(n))
|
||||
}
|
||||
}
|
||||
Command::from_str(name)
|
||||
.map_err(|_| ParseError {
|
||||
msg: format!("unknown command: {name}"),
|
||||
span: start..self.pos
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(msg: &str) -> Result<Message, ParseError> {
|
||||
let mut p = Parser {
|
||||
src: msg,
|
||||
chars: msg.chars().peekable(),
|
||||
pos: 0,
|
||||
};
|
||||
p.parse()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse, Message, Command, ParseError};
|
||||
|
||||
#[test]
|
||||
fn test() -> Result<(), ParseError> {
|
||||
assert_eq!(
|
||||
parse(":example.org PONG irc.example.com :irc.example.com")?,
|
||||
Message {
|
||||
prefix: Some("example.org"),
|
||||
command: Command::PONG,
|
||||
params: vec!["irc.example.com", "irc.example.com"],
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse("PASS AAAABBBB 0210-IRC+ ngIRCd|26.1:CHLMSXZ PZ")?,
|
||||
Message {
|
||||
prefix: None,
|
||||
command: Command::PASS,
|
||||
params: vec!["AAAABBBB", "0210-IRC+", "ngIRCd|26.1:CHLMSXZ", "PZ"],
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse(":example.org SERVER example.org 1 :Server Info Text")?,
|
||||
Message {
|
||||
prefix: Some("example.org"),
|
||||
command: Command::SERVER,
|
||||
params: vec!["example.org", "1", "Server Info Text"],
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
7
src/bridge_irc_sp/mod.rs
Normal file
7
src/bridge_irc_sp/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod config;
|
||||
mod bridge;
|
||||
mod network;
|
||||
mod message;
|
||||
|
||||
pub use config::IrcSpConfig;
|
||||
pub use bridge::IrcSpTask;
|
71
src/bridge_irc_sp/network.rs
Normal file
71
src/bridge_irc_sp/network.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use std::{fmt, io};
|
||||
|
||||
use tokio::io::{Lines, AsyncBufRead, AsyncBufReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use log::debug;
|
||||
|
||||
use super::message::{Message, parse, ParseError};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ReadMessageError {
|
||||
String(&'static str),
|
||||
ParseError(ParseError),
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for ReadMessageError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ReadMessageError::String(e) => write!(f, "{e}"),
|
||||
ReadMessageError::ParseError(e) => write!(f, "{e}"),
|
||||
ReadMessageError::Io(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ReadMessageError {}
|
||||
|
||||
|
||||
pub struct MessageReader<R: AsyncBufRead + Unpin> {
|
||||
reader: Lines<R>,
|
||||
buf: String,
|
||||
}
|
||||
|
||||
impl<R: AsyncBufRead + Unpin> MessageReader<R> {
|
||||
pub fn new(reader: R) -> Self {
|
||||
Self {
|
||||
reader: reader.lines(),
|
||||
buf: String::with_capacity(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_message(&mut self) -> Result<Message, ReadMessageError> {
|
||||
match self.reader.next_line().await {
|
||||
Ok(Some(line)) => {
|
||||
self.buf = line;
|
||||
parse(&self.buf).map_err(ReadMessageError::ParseError)
|
||||
}
|
||||
Ok(None) => Err(ReadMessageError::String("Lost connection to peer server")),
|
||||
Err(e) => Err(ReadMessageError::Io(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MessageWriter<W: AsyncWrite + Unpin> {
|
||||
writer: W,
|
||||
}
|
||||
|
||||
impl<W: AsyncWrite + Unpin> MessageWriter<W> {
|
||||
pub fn new(writer: W) -> Self {
|
||||
Self { writer }
|
||||
}
|
||||
|
||||
pub async fn write_msg<'a>(&mut self, msg: Message<'a>) -> Result<(), io::Error> {
|
||||
let msg = format!("{}\r\n", msg);
|
||||
debug!("sending message: {:?}", msg);
|
||||
self.writer.write_all(msg.as_bytes()).await
|
||||
}
|
||||
|
||||
pub async fn flush(&mut self) -> Result<(), io::Error> {
|
||||
self.writer.flush().await
|
||||
}
|
||||
|
||||
}
|
|
@ -22,6 +22,7 @@ pub struct MatrixTask {
|
|||
struct Context {
|
||||
id: Id,
|
||||
tx: Sender,
|
||||
suffix: Arc<str>,
|
||||
rooms: Arc<Linkmap<String>>,
|
||||
}
|
||||
|
||||
|
@ -41,6 +42,7 @@ impl Task for MatrixTask {
|
|||
client.add_event_handler_context(Arc::new(Context {
|
||||
id,
|
||||
tx,
|
||||
suffix: self.config.suffix.clone(),
|
||||
rooms: self.links.clone()
|
||||
}));
|
||||
client.add_event_handler(handle_matrix_msg);
|
||||
|
@ -99,6 +101,7 @@ async fn handle_matrix_msg(ev: SyncRoomMessageEvent, client: Client, room: Room,
|
|||
origin: (ctx.id, room.room_id().to_string()),
|
||||
link: link.clone(),
|
||||
author: sender.name().to_owned(),
|
||||
suffix: ctx.suffix.clone(),
|
||||
content: body.to_owned(),
|
||||
avatar,
|
||||
})?;
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::ruma::OwnedUserId;
|
||||
use serde::Deserialize;
|
||||
|
||||
fn default_suffix() -> Arc<str> {
|
||||
"[m]".into()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MatrixConfig {
|
||||
pub user: OwnedUserId,
|
||||
pub password: String,
|
||||
#[serde(default="default_suffix")]
|
||||
pub suffix: Arc<str>
|
||||
}
|
||||
|
|
|
@ -7,10 +7,12 @@ use crate::supervisor::Task;
|
|||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag="platform")]
|
||||
#[serde(rename_all="snake_case")]
|
||||
#[serde(rename_all="kebab-case")]
|
||||
pub enum Node {
|
||||
#[cfg(feature="irc")]
|
||||
Irc(crate::bridge_irc::IrcTask),
|
||||
#[cfg(feature="irc-sp")]
|
||||
IrcSp(crate::bridge_irc_sp::IrcSpTask),
|
||||
#[cfg(feature="matrix")]
|
||||
Matrix(crate::bridge_matrix::MatrixTask),
|
||||
#[cfg(feature="discord")]
|
||||
|
@ -22,6 +24,8 @@ impl Node {
|
|||
match self {
|
||||
#[cfg(feature="irc")]
|
||||
Node::Irc(t) => Box::new(t),
|
||||
#[cfg(feature="irc-sp")]
|
||||
Node::IrcSp(t) => Box::new(t),
|
||||
#[cfg(feature="matrix")]
|
||||
Node::Matrix(t) => Box::new(t),
|
||||
#[cfg(feature="discord")]
|
||||
|
|
|
@ -12,6 +12,8 @@ mod config;
|
|||
|
||||
#[cfg(feature="irc")]
|
||||
mod bridge_irc;
|
||||
#[cfg(feature="irc-sp")]
|
||||
mod bridge_irc_sp;
|
||||
#[cfg(feature="matrix")]
|
||||
mod bridge_matrix;
|
||||
#[cfg(feature="discord")]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::{fmt, error::Error, time::Duration, any::Any, panic::AssertUnwindSafe, collections::HashMap};
|
||||
use std::{fmt, error::Error, time::Duration, any::Any, panic::AssertUnwindSafe, collections::HashMap, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::{stream::FuturesUnordered, StreamExt, FutureExt};
|
||||
|
@ -36,6 +36,7 @@ pub struct Message {
|
|||
pub origin: (Id, String),
|
||||
pub link: Link,
|
||||
pub author: String,
|
||||
pub suffix: Arc<str>,
|
||||
pub content: String,
|
||||
pub avatar: Option<String>,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue