diff --git a/Cargo.toml b/Cargo.toml index 83882ef..02a2e76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ matrix-sdk = { version = "0.8", features = ["anyhow", "sso-login"] } percent-encoding = "2.3.1" rand_core = { version = "0.6", features = ["getrandom"] } regex = "1.8" -serde = "1.0" +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1.0.0", features = ["full"] } tokio-util = { version = "0.7", features = ["codec"] } diff --git a/src/args.rs b/src/args.rs index 9d223a3..bf2b01c 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,7 +1,26 @@ use clap::Parser; use lazy_static::lazy_static; +use serde::Deserialize; use std::net::SocketAddr; +#[derive(clap::ValueEnum, Clone, Debug, PartialEq, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AutoJoinOptions { + None, + Queries, + Channels, + All, +} + +impl AutoJoinOptions { + pub fn join_queries(&self) -> bool { + [AutoJoinOptions::Queries, AutoJoinOptions::All].contains(self) + } + pub fn join_channels(&self) -> bool { + [AutoJoinOptions::Channels, AutoJoinOptions::All].contains(self) + } +} + #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] pub struct Args { @@ -19,6 +38,9 @@ pub struct Args { #[arg(long, default_value = None)] pub media_url: Option, + + #[arg(long, value_enum, default_value_t = AutoJoinOptions::None)] + pub autojoin: AutoJoinOptions, } pub fn args() -> &'static Args { diff --git a/src/ircd/login.rs b/src/ircd/login.rs index 0c693ae..72d59e2 100644 --- a/src/ircd/login.rs +++ b/src/ircd/login.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Error, Result}; use irc::{client::prelude::Command, proto::IrcCodec}; +use crate::ircd::proto::{join, raw_msg}; use log::{debug, info, trace, warn}; use tokio::net::TcpStream; use tokio::sync::oneshot; @@ -53,6 +54,22 @@ pub async fn auth_loop( ))) .await?; info!("Processing login from {}!{}", nick, user); + // Promote matric to chan + let matrircchan = "matrirc".to_string(); + stream + .send(join( + Some(format!("{}!{}@matrirc", nick, user)), + "matrirc".to_string(), + )) + .await?; + stream + .send(join( + Some(format!("{}!{}@matrirc", nick, user)), + "matrirc".to_string(), + )) + .await?; + stream.send(raw_msg(format!(":matrirc 353 {} = {} :@matrirc", nick, matrircchan))).await?; + stream.send(raw_msg(format!(":matrirc 366 {} {} :End", nick, matrircchan))).await?; let client = match state::login(&nick, &pass)? { Some(session) => matrix_restore_session(stream, &nick, &pass, session).await?, None => matrix_login_loop(stream, &nick, &pass).await?, diff --git a/src/ircd/mod.rs b/src/ircd/mod.rs index f99af5f..68fe34b 100644 --- a/src/ircd/mod.rs +++ b/src/ircd/mod.rs @@ -90,6 +90,7 @@ async fn handle_client(mut stream: Framed) -> Result<()> { .irc() .send_privmsg("matrirc", &matrirc.irc().nick, "okay") .await?; + if let Err(e) = proto::ircd_sync_read(reader_stream, reader_matrirc).await { info!("irc read task failed: {:?}", e); } diff --git a/src/ircd/proto.rs b/src/ircd/proto.rs index 0c4c2ad..90e7826 100644 --- a/src/ircd/proto.rs +++ b/src/ircd/proto.rs @@ -10,6 +10,8 @@ use tokio::net::TcpStream; use tokio::sync::mpsc; use tokio_util::codec::Framed; +use crate::args::{args, AutoJoinOptions}; +use crate::matrix::room_mappings::RoomTargetType; use crate::{matrirc::Matrirc, matrix::MatrixMessageType}; /// it's a bit of a pain to redo the work twice for notice/privmsg, @@ -162,6 +164,44 @@ pub async fn ircd_sync_write( Ok(()) } +pub async fn join_channels(matrirc: &Matrirc) -> Result<()> { + let irc = matrirc.irc(); + let matrix = matrirc.matrix(); + let mapping = matrirc.mappings(); + + for joined in matrix.joined_rooms() { + if joined.is_tombstoned() { + trace!( + "Skipping tombstoned {}", + joined + .name() + .unwrap_or_else(|| joined.room_id().to_string()) + ); + continue; + } + let roomtarget = mapping.try_room_target(&joined).await?; + let chantype = roomtarget.target_type().await; + if [RoomTargetType::Chan, RoomTargetType::LeftChan].contains(&chantype) + && args().autojoin.join_channels() + { + roomtarget.join_chan(irc).await; + } else if chantype == RoomTargetType::Query { + let name = roomtarget.target().await; + let _ = irc.send(join(Some(format!("{}!{}@matrirc", name, name)), "matrirc".to_string())).await?; + if args().autojoin.join_queries() { + let _ = irc + .send(privmsg( + name, + &irc.nick, + "* ", + )) + .await; + } + } + } + Ok(()) +} + pub async fn ircd_sync_read( mut reader: SplitStream>, matrirc: Matrirc, diff --git a/src/matrix/mod.rs b/src/matrix/mod.rs index 562c9d8..ba4b072 100644 --- a/src/matrix/mod.rs +++ b/src/matrix/mod.rs @@ -2,6 +2,7 @@ use anyhow::Result; use log::warn; use matrix_sdk::{config::SyncSettings, LoopCtrl}; +use crate::ircd::proto; use crate::matrirc::{Matrirc, Running}; mod invite; @@ -38,7 +39,13 @@ pub async fn matrix_sync(matrirc: Matrirc) -> Result<()> { // XXX send to irc Ok(LoopCtrl::Break) } else { - Ok(LoopCtrl::Continue) + match proto::join_channels(loop_matrirc).await { + Ok(_) => Ok(LoopCtrl::Continue), + Err(e) => { + warn!("Failed to autojoin rooms in IRC: {}", e); + Ok(LoopCtrl::Break) + } + } } } Running::Continue => Ok(LoopCtrl::Continue), diff --git a/src/matrix/room_mappings.rs b/src/matrix/room_mappings.rs index ee17e42..422d182 100644 --- a/src/matrix/room_mappings.rs +++ b/src/matrix/room_mappings.rs @@ -56,8 +56,8 @@ pub struct RoomTarget { inner: Arc>, } -#[derive(Debug, PartialEq)] -enum RoomTargetType { +#[derive(Debug, PartialEq, Clone)] +pub enum RoomTargetType { /// room maps to a query e.g. single other member (or alone!) Query, /// room maps to a chan, and irc side has it joined @@ -213,18 +213,21 @@ impl RoomTarget { pub async fn target(&self) -> String { self.inner.read().await.target.clone() } + pub async fn target_type(&self) -> RoomTargetType { + self.inner.read().await.target_type.clone() + } - async fn join_chan(&self, irc: &IrcClient) -> bool { + pub async fn join_chan(&self, irc: &IrcClient) -> bool { let mut lock = self.inner.write().await; - match &lock.target_type { - RoomTargetType::LeftChan => (), - RoomTargetType::Query => (), + let prefix = match &lock.target_type { + RoomTargetType::LeftChan => "#", + RoomTargetType::Query => "", // got raced or already joined RoomTargetType::JoiningChan => return false, RoomTargetType::Chan => return false, }; lock.target_type = RoomTargetType::JoiningChan; - let chan = format!("#{}", lock.target); + let chan = format!("{}{}", prefix, lock.target); drop(lock); // we need to initate the join before getting members in another task @@ -458,7 +461,7 @@ impl Mappings { // long enough to check for deduplicate and it's a bit of a mess; it could be done // with a more generic 'insert_free_target' that takes a couple of callbacks but // it's just not worth it - async fn try_room_target(&self, room: &Room) -> Result { + pub async fn try_room_target(&self, room: &Room) -> Result { // happy case first if let Some(target) = self.inner.read().await.rooms.get(room.room_id()) { return Ok(target.clone());