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",
|
"matrix-sdk",
|
||||||
"serde",
|
"serde",
|
||||||
"serenity",
|
"serenity",
|
||||||
|
"strum",
|
||||||
|
"strum_macros",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml 0.7.4",
|
"toml 0.7.4",
|
||||||
]
|
]
|
||||||
|
@ -1010,6 +1012,12 @@ dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
|
@ -2382,6 +2390,12 @@ dependencies = [
|
||||||
"untrusted",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.13"
|
version = "1.0.13"
|
||||||
|
@ -2650,6 +2664,28 @@ version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
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]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
|
@ -8,6 +8,7 @@ readme = "README.md"
|
||||||
[features]
|
[features]
|
||||||
# platforms
|
# platforms
|
||||||
irc = ["dep:irc"]
|
irc = ["dep:irc"]
|
||||||
|
irc-sp = ["tokio/net"]
|
||||||
matrix = ["dep:matrix-sdk"]
|
matrix = ["dep:matrix-sdk"]
|
||||||
discord = ["dep:serenity"]
|
discord = ["dep:serenity"]
|
||||||
# tls
|
# tls
|
||||||
|
@ -16,7 +17,7 @@ tls-rustls = ["irc?/tls-rust", "matrix-sdk?/rustls-tls", "serenity?/rustls_bac
|
||||||
# extra
|
# extra
|
||||||
matrix-e2ee = ["matrix", "matrix-sdk?/e2e-encryption"]
|
matrix-e2ee = ["matrix", "matrix-sdk?/e2e-encryption"]
|
||||||
# default
|
# default
|
||||||
default = ["tls-rustls", "irc", "discord", "matrix", "matrix-e2ee"]
|
default = ["tls-rustls", "irc", "irc-sp", "discord", "matrix", "matrix-e2ee"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
|
@ -26,6 +27,8 @@ log = "0.4"
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
tokio = { version = "1.28", features = ["rt-multi-thread", "macros", "time", "sync"] }
|
tokio = { version = "1.28", features = ["rt-multi-thread", "macros", "time", "sync"] }
|
||||||
toml = "0.7"
|
toml = "0.7"
|
||||||
|
strum_macros = "0.24"
|
||||||
|
strum = { version = "0.24", features = ["derive"] }
|
||||||
|
|
||||||
[dependencies.irc]
|
[dependencies.irc]
|
||||||
optional = true
|
optional = true
|
||||||
|
|
|
@ -21,6 +21,7 @@ impl Task for DiscordTask {
|
||||||
async fn start(&self, id: Id, tx: Sender, mut rx: Receiver) -> TaskResult {
|
async fn start(&self, id: Id, tx: Sender, mut rx: Receiver) -> TaskResult {
|
||||||
let handler = Handler {
|
let handler = Handler {
|
||||||
links: self.links.clone(),
|
links: self.links.clone(),
|
||||||
|
suffix: self.config.suffix.clone(),
|
||||||
tx,
|
tx,
|
||||||
id,
|
id,
|
||||||
};
|
};
|
||||||
|
@ -64,7 +65,7 @@ impl DiscordTask {
|
||||||
hook.execute(http, false, |hook| {
|
hook.execute(http, false, |hook| {
|
||||||
hook.username(&msg.author)
|
hook.username(&msg.author)
|
||||||
.content(&msg.content)
|
.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?;
|
}).await?;
|
||||||
} else {
|
} else {
|
||||||
channel.say(http, &content).await?;
|
channel.say(http, &content).await?;
|
||||||
|
@ -76,6 +77,7 @@ impl DiscordTask {
|
||||||
|
|
||||||
struct Handler {
|
struct Handler {
|
||||||
id: Id,
|
id: Id,
|
||||||
|
suffix: Arc<str>,
|
||||||
links: Arc<Linkmap<ChannelId>>,
|
links: Arc<Linkmap<ChannelId>>,
|
||||||
tx: Sender,
|
tx: Sender,
|
||||||
}
|
}
|
||||||
|
@ -98,6 +100,7 @@ impl EventHandler for Handler {
|
||||||
origin: (self.id, msg.channel_id.to_string()),
|
origin: (self.id, msg.channel_id.to_string()),
|
||||||
link: link.clone(),
|
link: link.clone(),
|
||||||
author,
|
author,
|
||||||
|
suffix: self.suffix.clone(),
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
avatar,
|
avatar,
|
||||||
}).expect("failed to broadcast message");
|
}).expect("failed to broadcast message");
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serenity::model::prelude::{WebhookId, ChannelId};
|
use serenity::model::prelude::{WebhookId, ChannelId};
|
||||||
|
|
||||||
|
fn default_suffix() -> Arc<str> {
|
||||||
|
"[d]".into()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct DiscordConfig {
|
pub struct DiscordConfig {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub webhooks: HashMap<ChannelId, WebhookId>,
|
pub webhooks: HashMap<ChannelId, WebhookId>,
|
||||||
|
#[serde(default="default_suffix")]
|
||||||
|
pub suffix: Arc<str>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ impl IrcTask {
|
||||||
origin: (id, channel.clone()),
|
origin: (id, channel.clone()),
|
||||||
link: link.clone(),
|
link: link.clone(),
|
||||||
author: author.to_owned(),
|
author: author.to_owned(),
|
||||||
|
suffix: self.config.suffix.clone(),
|
||||||
content: message.clone(),
|
content: message.clone(),
|
||||||
avatar: None,
|
avatar: None,
|
||||||
})?;
|
})?;
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
const fn default_port() -> u16 { 6667 }
|
const fn default_port() -> u16 { 6667 }
|
||||||
const fn default_tls() -> bool { true }
|
const fn default_tls() -> bool { true }
|
||||||
|
fn default_suffix() -> Arc<str> {
|
||||||
|
"[i]".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct IrcConfig {
|
pub struct IrcConfig {
|
||||||
|
@ -13,4 +19,6 @@ pub struct IrcConfig {
|
||||||
pub nick: String,
|
pub nick: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub alt_nicks: Vec<String>,
|
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 {
|
struct Context {
|
||||||
id: Id,
|
id: Id,
|
||||||
tx: Sender,
|
tx: Sender,
|
||||||
|
suffix: Arc<str>,
|
||||||
rooms: Arc<Linkmap<String>>,
|
rooms: Arc<Linkmap<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +42,7 @@ impl Task for MatrixTask {
|
||||||
client.add_event_handler_context(Arc::new(Context {
|
client.add_event_handler_context(Arc::new(Context {
|
||||||
id,
|
id,
|
||||||
tx,
|
tx,
|
||||||
|
suffix: self.config.suffix.clone(),
|
||||||
rooms: self.links.clone()
|
rooms: self.links.clone()
|
||||||
}));
|
}));
|
||||||
client.add_event_handler(handle_matrix_msg);
|
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()),
|
origin: (ctx.id, room.room_id().to_string()),
|
||||||
link: link.clone(),
|
link: link.clone(),
|
||||||
author: sender.name().to_owned(),
|
author: sender.name().to_owned(),
|
||||||
|
suffix: ctx.suffix.clone(),
|
||||||
content: body.to_owned(),
|
content: body.to_owned(),
|
||||||
avatar,
|
avatar,
|
||||||
})?;
|
})?;
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use matrix_sdk::ruma::OwnedUserId;
|
use matrix_sdk::ruma::OwnedUserId;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
fn default_suffix() -> Arc<str> {
|
||||||
|
"[m]".into()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct MatrixConfig {
|
pub struct MatrixConfig {
|
||||||
pub user: OwnedUserId,
|
pub user: OwnedUserId,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
#[serde(default="default_suffix")]
|
||||||
|
pub suffix: Arc<str>
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,12 @@ use crate::supervisor::Task;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(tag="platform")]
|
#[serde(tag="platform")]
|
||||||
#[serde(rename_all="snake_case")]
|
#[serde(rename_all="kebab-case")]
|
||||||
pub enum Node {
|
pub enum Node {
|
||||||
#[cfg(feature="irc")]
|
#[cfg(feature="irc")]
|
||||||
Irc(crate::bridge_irc::IrcTask),
|
Irc(crate::bridge_irc::IrcTask),
|
||||||
|
#[cfg(feature="irc-sp")]
|
||||||
|
IrcSp(crate::bridge_irc_sp::IrcSpTask),
|
||||||
#[cfg(feature="matrix")]
|
#[cfg(feature="matrix")]
|
||||||
Matrix(crate::bridge_matrix::MatrixTask),
|
Matrix(crate::bridge_matrix::MatrixTask),
|
||||||
#[cfg(feature="discord")]
|
#[cfg(feature="discord")]
|
||||||
|
@ -22,6 +24,8 @@ impl Node {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature="irc")]
|
#[cfg(feature="irc")]
|
||||||
Node::Irc(t) => Box::new(t),
|
Node::Irc(t) => Box::new(t),
|
||||||
|
#[cfg(feature="irc-sp")]
|
||||||
|
Node::IrcSp(t) => Box::new(t),
|
||||||
#[cfg(feature="matrix")]
|
#[cfg(feature="matrix")]
|
||||||
Node::Matrix(t) => Box::new(t),
|
Node::Matrix(t) => Box::new(t),
|
||||||
#[cfg(feature="discord")]
|
#[cfg(feature="discord")]
|
||||||
|
|
|
@ -12,6 +12,8 @@ mod config;
|
||||||
|
|
||||||
#[cfg(feature="irc")]
|
#[cfg(feature="irc")]
|
||||||
mod bridge_irc;
|
mod bridge_irc;
|
||||||
|
#[cfg(feature="irc-sp")]
|
||||||
|
mod bridge_irc_sp;
|
||||||
#[cfg(feature="matrix")]
|
#[cfg(feature="matrix")]
|
||||||
mod bridge_matrix;
|
mod bridge_matrix;
|
||||||
#[cfg(feature="discord")]
|
#[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 async_trait::async_trait;
|
||||||
use futures::{stream::FuturesUnordered, StreamExt, FutureExt};
|
use futures::{stream::FuturesUnordered, StreamExt, FutureExt};
|
||||||
|
@ -36,6 +36,7 @@ pub struct Message {
|
||||||
pub origin: (Id, String),
|
pub origin: (Id, String),
|
||||||
pub link: Link,
|
pub link: Link,
|
||||||
pub author: String,
|
pub author: String,
|
||||||
|
pub suffix: Arc<str>,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue