diff --git a/packages/core/core-shared/src/actor.rs b/packages/core/core-shared/src/actor.rs index fca991b..5207a91 100644 --- a/packages/core/core-shared/src/actor.rs +++ b/packages/core/core-shared/src/actor.rs @@ -1,18 +1,18 @@ use std::{fmt, time::Duration}; use futures::{FutureExt, future::FusedFuture}; -use rand::{SeedableRng, rngs::SmallRng}; #[cfg(not(feature = "web"))] use std::time::Instant; #[cfg(feature = "web")] use web_time::Instant; #[cfg(feature = "web")] +#[allow(unused_imports)] use crate::dbg; use crate::{ SendCommand, database::Database, - response_channels::{CommandKey, CommandResponse, ResponseChannels}, + response_channels::{CommandResponse, ResponseChannels}, state::{Channel, Message, OrbitError, Server, ServerEvent, User}, }; use anyhow::Context; @@ -79,9 +79,24 @@ pub trait IrcConnection: fmt::Debug { fn address(&self) -> &str; } -pub(crate) struct RequestedHistory { +#[derive(Debug)] +pub(crate) struct RequestedBatch { pub target: String, pub label: Option, + pub typ: BatchType, +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum BatchType { + Join, + JoinHistory, + History, +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum HistoryPurpose { + Join, + History, } #[derive(Debug)] @@ -93,6 +108,7 @@ pub(crate) struct CurrentBatch { #[derive(Debug)] pub(crate) enum BatchData { History { + purpose: HistoryPurpose, label: Option, target: String, messages: Vec, @@ -101,6 +117,10 @@ pub(crate) enum BatchData { target: String, message: Message, }, + Join { + label: String, + target: String, + }, Unhandled, } @@ -108,6 +128,9 @@ impl CurrentBatch { pub fn is_chathistory(&self) -> bool { matches!(self.data, BatchData::History { .. }) } + pub fn is_join(&self) -> bool { + matches!(self.data, BatchData::Join { .. }) + } } #[derive(Default, Clone)] @@ -134,9 +157,8 @@ pub struct IrcActor { pub(crate) disconnect_handlers: Vec>, pub(crate) current_batches: Vec, - pub(crate) requested_history_batches: Vec<(RequestedHistory, Instant)>, + pub(crate) requested_batches: Vec<(RequestedBatch, Instant)>, pub(crate) sasl_state: SaslState, - pub(crate) rng: SmallRng, } impl IrcActor { @@ -162,21 +184,14 @@ impl IrcActor { error_handlers: Default::default(), disconnect_handlers: Default::default(), current_batches: Default::default(), - requested_history_batches: Default::default(), + requested_batches: Default::default(), sasl_state: Default::default(), - rng: SmallRng::from_seed([1; 32]), }; - let (tx, rx) = oneshot::channel(); - actor - .response_channels - .register(CommandKey::RequestCaps, tx); - actor.request_caps().await?; + actor.init_cap_request().await?; spawn(actor); - rx.await.unwrap(); - Ok(cmd_tx) } @@ -220,7 +235,7 @@ impl IrcActor { assert!( - self.requested_history_batches + self.requested_batches .iter() .all(|(_, creation)| creation.elapsed() < Duration::from_secs(5)) ); @@ -282,12 +297,17 @@ impl IrcActor { } #[tracing::instrument(err, skip(self))] - pub(crate) async fn request_caps(&mut self) -> Result<(), OrbitError> { + pub(crate) async fn init_cap_request(&mut self) -> Result<(), OrbitError> { let irc_version = String::from("302"); self.cap_ls(irc_version) .await .context("Failed to send CAPS LS")?; - self.cap_req(&[ + + Ok(()) + } + pub(crate) async fn request_caps(&mut self) -> Result<(), OrbitError> { + let mut enable = Vec::new(); + for cap in [ "echo-message", "labeled-response", "message-tags", @@ -298,11 +318,24 @@ impl IrcActor { "draft/event-playback", "draft/account-registration", "draft/multiline", + "draft/extended-isupport", "server-time", "batch", - ]) - .await - .context("Failed to send CAP REQ")?; + "draft/webpush", + "extended-monitor", + "away-notify", + "draft/read-marker", + ] { + if self.state.capabilities.cap_by_name(cap).has { + enable.push(cap); + } + } + + if !enable.is_empty() { + self.cap_req(&enable) + .await + .context("Failed to send CAP REQ")?; + } Ok(()) } @@ -314,7 +347,6 @@ impl IrcActor { username: String, realname: String, ) -> Result<(), OrbitError> { - self.cap_end().await.context("Failed to send CAP END")?; self.nick(nickname.clone()) .await .context("Failed to send NICK")?; diff --git a/packages/core/core-shared/src/handlers.rs b/packages/core/core-shared/src/handlers.rs index 6aba606..0d3241b 100644 --- a/packages/core/core-shared/src/handlers.rs +++ b/packages/core/core-shared/src/handlers.rs @@ -1,28 +1,30 @@ +use anyhow::{Context, anyhow}; +use base64::prelude::*; +use irc_proto::{BatchSubCommand, CapSubCommand, Command::*, Message as IrcMessage, Response}; use std::collections::HashMap; #[cfg(not(feature = "web"))] use std::time::Instant; +use tracing::{debug, error, warn}; #[cfg(feature = "web")] use web_time::Instant; -#[cfg(feature = "web")] -use crate::dbg; use crate::{ SendCommand, actor::{ - ActorCommand, ActorMessage, BatchData, CurrentBatch, IrcActor, IrcConnection, - RequestedHistory, SaslState, + ActorCommand, ActorMessage, BatchData, BatchType, CurrentBatch, HistoryPurpose, IrcActor, + IrcConnection, RequestedBatch, SaslState, }, database::Database, - response_channels::{CommandKey, CommandResponse, generate_label}, + response_channels::{CommandKey, CommandResponse}, state::{ Channel, ChannelRole, ChannelUser, History, Message, MessageMetadata, MessageReference, MessageType, OrbitError, ServerEvent, SignedIn, Tags, TextMessage, User, }, }; -use anyhow::{Context, anyhow}; -use base64::prelude::*; -use irc_proto::{BatchSubCommand, CapSubCommand, Command::*, Message as IrcMessage, Response}; -use tracing::{debug, error, warn}; + +#[cfg(feature = "web")] +#[allow(unused_imports)] +use crate::dbg; impl IrcActor { #[tracing::instrument(err, skip(self))] @@ -97,7 +99,7 @@ impl IrcActor { } CapSubCommand::LS if let Some(param) = param => { if param == "*" { - return Ok(()); + unreachable!("that should mean that caps is Some"); } for cap in param.split_whitespace() { let cap = cap @@ -106,14 +108,14 @@ impl IrcActor { .ok_or_else(|| anyhow!("Cap is empty: \"{}\"", cap))?; self.state.capabilities.set_from_name(cap, None); } + + self.request_caps().await?; } CapSubCommand::ACK if let Some(param) = param => { for cap in param.split_whitespace() { self.state.capabilities.set_from_name(cap, Some(true)); } - self.response_channels - .reply(&CommandKey::RequestCaps, CommandResponse::Capabilities) - .unwrap(); + self.cap_end().await.context("Failed to send CAP END")?; } _ => { debug!("unhandled caps message"); @@ -168,31 +170,34 @@ impl IrcActor { .insert(target.to_string(), channel.clone()); if !self.state.capabilities.history.enabled { - self.response_channels - .reply( - &CommandKey::Join(target.to_string()), - CommandResponse::Join(target.to_string()), - ) - .map_err(|e| anyhow!("Failed to reply to JOIN command {e:?}"))?; - } + if !self.state.capabilities.labeled_response.enabled { + let channel = self + .state + .channels + .get(target) + .expect("should exist after just joining"); + self.response_channels + .reply( + &CommandKey::Join(target.to_string()), + CommandResponse::Join(Box::new(channel.clone())), + ) + .map_err(|e| anyhow!("Failed to reply to JOIN command {e:?}"))?; + } - self.on_event(ServerEvent::Joined(channel)).await?; + self.on_event(ServerEvent::Joined(channel)).await?; + } if self.state.capabilities.history.enabled { - let label = if self.state.capabilities.labeled_response.enabled { - Some(generate_label(&mut self.rng)) - } else { - None - }; - - self.requested_history_batches.push(( - RequestedHistory { + self.requested_batches.push(( + RequestedBatch { target: target.to_string(), - label: label.clone(), + label: tags.label.clone(), + typ: BatchType::JoinHistory, }, Instant::now(), )); - self.history_latest(target.to_string(), None, 5, label) + + self.history_latest(target.to_string(), None, 5, tags.label) .await .context("Failed to request latest history")?; } @@ -470,17 +475,22 @@ impl IrcActor { match typ { Some(BatchSubCommand::CUSTOM(c)) if c.as_str() == "CHATHISTORY" => { let idx = self - .requested_history_batches + .requested_batches .iter() .position(|b| b.0.label == tags.label) .expect("Chat history was requested"); - let channel = self.requested_history_batches.remove(idx).0.target; + let request = self.requested_batches.remove(idx).0; self.current_batches.push(CurrentBatch { id: id.to_string(), data: BatchData::History { + purpose: match request.typ { + BatchType::Join => unreachable!("not a chathistory type"), + BatchType::JoinHistory => HistoryPurpose::Join, + BatchType::History => HistoryPurpose::History, + }, label: tags.label, - target: channel, + target: request.target, messages: Vec::new(), }, }); @@ -520,6 +530,27 @@ impl IrcActor { }, }); } + Some(BatchSubCommand::CUSTOM(c)) if c.as_str() == "LABELED-RESPONSE" => { + let idx = self + .requested_batches + .iter() + .position(|b| b.0.label == tags.label) + .expect("labeled response was requested"); + let RequestedBatch { target, label, typ } = + self.requested_batches.remove(idx).0; + + if typ == BatchType::Join { + self.current_batches.push(CurrentBatch { + id: id.to_string(), + data: BatchData::Join { + label: label.expect("a labeled response should have a label"), + target, + }, + }); + } else { + debug!("Unhandled: {:?}", typ); + } + } _ => { self.current_batches.push(CurrentBatch { id: id.to_string(), @@ -537,6 +568,7 @@ impl IrcActor { if let Some(batch) = self.current_batches.pop() { match batch.data { BatchData::History { + purpose: typ, label, target: channel_name, messages, @@ -547,29 +579,47 @@ impl IrcActor { .await?; } - let history = History { - channel: channel_name.clone(), - messages, - }; - - let key = if let Some(label) = label { - CommandKey::Label(label) - } else { - CommandKey::History - }; - - self.response_channels - .reply( - &CommandKey::Join(channel_name.clone()), - CommandResponse::Join(channel_name.clone()), - ) - .map_err(|e| anyhow!("Failed to reply to JOIN command {e:?}"))?; - - self.response_channels - .reply(&key, CommandResponse::History(history.clone())) - .unwrap(); + match typ { + HistoryPurpose::History => { + let key = if let Some(label) = label { + CommandKey::Label(label) + } else { + CommandKey::History(channel_name.clone()) + }; + + self.response_channels + .reply( + &key, + CommandResponse::History(History { + target: channel_name, + messages, + }), + ) + .map_err(|e| { + anyhow!("Failed to reply to History command {e:?}") + })?; + } + HistoryPurpose::Join => { + let key = if let Some(label) = label { + CommandKey::Label(label) + } else { + CommandKey::Join(channel_name.clone()) + }; + + let channel = self + .state + .channels + .get(&channel_name) + .expect("should exist after just joining"); + + self.response_channels + .reply(&key, CommandResponse::Join(Box::new(channel.clone()))) + .map_err(|e| { + anyhow!("Failed to reply to Join command {e:?}") + })?; + } + } } - BatchData::Multiline { target, message: state_message, @@ -610,6 +660,20 @@ impl IrcActor { }) .await?; } + BatchData::Join { label, target } => { + let channel = self + .state + .channels + .get(&target) + .expect("should exist after just joining"); + + self.response_channels + .reply( + &CommandKey::Label(label), + CommandResponse::Join(Box::new(channel.clone())), + ) + .map_err(|e| anyhow!("Failed to reply to JOIN command {e:?}"))?; + } BatchData::Unhandled => (), } } @@ -722,19 +786,14 @@ impl IrcActor { .map_err(|e| anyhow!("Failed to send server event {e:?}"))?, Response::RPL_SASLSUCCESS => { self.response_channels - .reply( - &CommandKey::SignIn, - CommandResponse::SignIn(Ok(SignedIn::User)), - ) + .reply(&CommandKey::SignIn, CommandResponse::SignIn(SignedIn::User)) .map_err(|e| anyhow!("Failed to reply to sign in command {e:?}"))?; - - self.cap_end().await.context("Failed to send CAP END")?; } Response::RPL_WELCOME => { self.response_channels .reply( &CommandKey::SignIn, - CommandResponse::SignIn(Ok(SignedIn::Guest)), + CommandResponse::SignIn(SignedIn::Guest), ) .map_err(|e| anyhow!("Failed to reply to sign in command {e:?}"))?; } @@ -745,7 +804,7 @@ impl IrcActor { self.response_channels .reply( &CommandKey::SignIn, - CommandResponse::SignIn(Err(OrbitError::SaslFailed(params[1].to_string()))), + CommandResponse::Error(OrbitError::SaslFailed(params[1].to_string())), ) .map_err(|e| anyhow!("Failed to reply to sign in command {e:?}"))?; } @@ -753,7 +812,7 @@ impl IrcActor { self.response_channels .reply( &CommandKey::SignIn, - CommandResponse::SignIn(Err(OrbitError::NickTaken)), + CommandResponse::Error(OrbitError::NickTaken), ) .map_err(|e| anyhow!("Failed to reply to sign in command {e:?}"))?; } @@ -765,9 +824,11 @@ impl IrcActor { channel.metadata.topic = Some(topic); let metadata = channel.metadata.clone(); - self.on_event(ServerEvent::ChannelUpdated(metadata)) - .await - .unwrap(); + if !self.current_batches.iter().any(|b| b.is_join()) { + self.on_event(ServerEvent::ChannelUpdated(metadata)) + .await + .unwrap(); + } } Response::RPL_NAMREPLY => { let channel_name = params[2].to_string(); @@ -802,16 +863,19 @@ impl IrcActor { channel.users = channel_users; } - Response::RPL_ENDOFNAMES => self - .on_event(ServerEvent::UserList { - channel: params[1].to_string(), - users: self.state.channels.get(¶ms[1]).unwrap().users.clone(), - }) - .await - .map_err(|e| anyhow!("Failed to send server event {e:?}"))?, + Response::RPL_ENDOFNAMES => { + if !self.current_batches.iter().any(|b| b.is_join()) { + self.on_event(ServerEvent::UserList { + channel: params[1].to_string(), + users: self.state.channels.get(¶ms[1]).unwrap().users.clone(), + }) + .await + .map_err(|e| anyhow!("Failed to send server event {e:?}"))? + } + } Response::RPL_ISUPPORT => { for option in ¶ms[1..(params.len() - 1)] { - let (key, value) = option.split_once('=').unzip(); + let (key, value) = option.trim().split_once('=').unzip(); self.state.support.set(key.unwrap_or(option), value); } self.state.metadata.name = self.state.support.network.clone(); @@ -884,18 +948,71 @@ impl IrcActor { self.sign_in_anonymous(nick, user, realname).await?; } ActorCommand::Join { channel, password } => { - self.response_channels - .register(CommandKey::Join(channel.clone()), cmd.reply_tx.unwrap()); - self.join(channel, password).await.unwrap(); + let label = if self.state.capabilities.labeled_response.enabled { + let label = self + .response_channels + .register_labeled(cmd.reply_tx.unwrap()); + self.requested_batches.push(( + RequestedBatch { + target: channel.to_string(), + label: Some(label.clone()), + typ: BatchType::Join, + }, + Instant::now(), + )); + + Some(label) + } else { + self.response_channels + .register(CommandKey::Join(channel.clone()), cmd.reply_tx.unwrap()); + + None + }; + self.join(channel, password, label).await.unwrap(); } ActorCommand::Privmsg { text, target } => { - self.response_channels.register( - CommandKey::Privmsg { - target: target.clone(), - text: text.clone(), - }, - cmd.reply_tx.unwrap(), - ); + if self.state.capabilities.echo_messages.enabled { + self.response_channels.register( + CommandKey::Privmsg { + target: target.clone(), + text: text.clone(), + }, + cmd.reply_tx.unwrap(), + ); + } else { + let tags = Tags::default(); + let nickname = &self.state.me.as_ref().unwrap().nickname; + + let state_message = Message { + text: Some(TextMessage { + content: text.clone(), + ..Default::default() + }), + metadata: MessageMetadata { + msgid: tags.msgid_with_fallback(&[ + &self.state.id.to_string(), + "PRIVMSG", + nickname, + &target, + text.as_ref(), + ]), + server_time: tags.server_time_with_fallback() as f64, + message_type: MessageType::Privmsg, + user: nickname.to_string(), + }, + }; + + cmd.reply_tx + .unwrap() + .send(CommandResponse::Privmsg(Box::new(state_message.clone()))) + .unwrap(); + + self.on_event(ServerEvent::Privmsg { + channel: target.to_string(), + message: state_message, + }) + .await?; + } self.privmsg(target, text).await.unwrap(); } ActorCommand::AddEventHandler { handler } => { @@ -911,6 +1028,16 @@ impl IrcActor { channel, before_msgid, } => { + if !self.state.capabilities.history.enabled { + cmd.reply_tx + .unwrap() + .send(CommandResponse::Error(OrbitError::CapabilityDisabled( + "chathistory", + ))) + .unwrap(); + return Ok(()); + } + let label = if self.state.capabilities.labeled_response.enabled { Some( self.response_channels @@ -918,15 +1045,16 @@ impl IrcActor { ) } else { self.response_channels - .register(CommandKey::History, cmd.reply_tx.unwrap()); + .register(CommandKey::History(channel.clone()), cmd.reply_tx.unwrap()); None }; - self.requested_history_batches.push(( - RequestedHistory { + self.requested_batches.push(( + RequestedBatch { target: channel.clone(), label: label.clone(), + typ: BatchType::History, }, Instant::now(), )); diff --git a/packages/core/core-shared/src/response_channels.rs b/packages/core/core-shared/src/response_channels.rs index 2bf73a9..bfc309c 100644 --- a/packages/core/core-shared/src/response_channels.rs +++ b/packages/core/core-shared/src/response_channels.rs @@ -6,19 +6,21 @@ use std::time::Instant; #[cfg(feature = "web")] use web_time::Instant; -#[cfg(feature = "web")] -use crate::dbg; -use crate::state::{Channel, History, Message, OrbitError, Server, SignedIn}; use futures::channel::oneshot; use tracing::warn; +use crate::state::{Channel, History, Message, OrbitError, Server, SignedIn}; + +#[cfg(feature = "web")] +#[allow(unused_imports)] +use crate::dbg; + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum CommandKey { - RequestCaps, SignIn, Join(String), Privmsg { target: String, text: String }, - History, + History(String), Label(String), } @@ -27,10 +29,11 @@ pub enum CommandResponse { GetState(Box), GetChannelState(Box>), Capabilities, - SignIn(Result), - Join(String), + SignIn(SignedIn), + Join(Box), Privmsg(Box), History(History), + Error(OrbitError), } const LABEL_CHARSET: &str = "abcdefghijklmnopqrstuvwxyz\ @@ -104,6 +107,6 @@ impl ResponseChannels { pub fn check_timeouts(&mut self) { self.channels - .retain(|(_, creation, _)| creation.elapsed() < Duration::from_secs(1)); + .retain(|(_, creation, _)| creation.elapsed() < Duration::from_secs(5)); } } diff --git a/packages/core/core-shared/src/send_command.rs b/packages/core/core-shared/src/send_command.rs index 57194b7..18bd6c3 100644 --- a/packages/core/core-shared/src/send_command.rs +++ b/packages/core/core-shared/src/send_command.rs @@ -6,6 +6,10 @@ use futures::{ }; use irc_proto::{CapSubCommand, Command::*, Message as IrcMessage, message::Tag}; +#[cfg(feature = "web")] +#[allow(unused_imports)] +use crate::dbg; + pub trait SendCommand { type Error: std::error::Error + Send + Sync + 'static; @@ -95,8 +99,9 @@ pub trait SendCommand { &mut self, channel: String, password: Option, + label: Option, ) -> impl std::future::Future> { - async { self.command(JOIN(channel, password, None), None).await } + async { self.command(JOIN(channel, password, None), label).await } } fn privmsg( diff --git a/packages/core/core-shared/src/state.rs b/packages/core/core-shared/src/state.rs index a84a8ff..3459cdd 100644 --- a/packages/core/core-shared/src/state.rs +++ b/packages/core/core-shared/src/state.rs @@ -1,19 +1,22 @@ use std::collections::HashMap; + use std::str::FromStr; -#[cfg(feature = "web")] -use crate::dbg; use irc_proto::message::Tag; use serde::{Deserialize, Serialize}; use thiserror::Error; use time::OffsetDateTime; use time::format_description::well_known::Iso8601; -use tracing::{error, warn}; +use tracing::{debug, error, warn}; #[cfg(feature = "web")] use tsify::Tsify; #[cfg(feature = "web")] use wasm_bindgen::prelude::*; +#[cfg(feature = "web")] +#[allow(unused_imports)] +use crate::dbg; + #[derive(Debug, Clone, PartialEq, Eq)] pub struct Server { pub id: i32, @@ -142,14 +145,15 @@ pub struct Capabilities { pub(crate) pre_away: Capability, pub(crate) read_marker: Capability, pub(crate) relaymsg: Capability, - pub(crate) ergo_nope: Capability, pub(crate) extended_join: Capability, pub(crate) extended_monitor: Capability, pub(crate) invite_notify: Capability, + // Depends on batch pub(crate) labeled_response: Capability, pub(crate) multi_prefix: Capability, pub(crate) setname: Capability, pub(crate) standard_replies: Capability, + pub(crate) tls: Capability, pub(crate) userhost_in_names: Capability, } @@ -162,6 +166,47 @@ pub struct Capability { } impl Capabilities { + pub fn cap_by_name(&self, cap: &str) -> &Capability { + match cap { + "message-tags" => &self.message_tags, + "draft/message-redaction" => &self.message_redaction, + "draft/multiline" => &self.multiline, + "draft/metadata-2" => &self.metadata, + "draft/webpush" => &self.webpush, + + "echo-message" => &self.echo_messages, + "sasl" => &self.sasl, + "draft/chathistory" => &self.history, + "draft/event-playback" => &self.event_playback, + "draft/account-registration" => &self.account_registration, + "server-time" => &self.server_time, + + "account-notify" => &self.account_notify, + "account-tag" => &self.account_tag, + "away-notify" => &self.away_notify, + "batch" => &self.batch, + "cap-notify" => &self.cap_notify, + "chghost" => &self.chghost, + "draft/channel-rename" => &self.channel_rename, + "draft/extended-isupport" => &self.extended_isupport, + "draft/languages" => &self.languages, + "no-implicit-names" | "draft/no-implicit-names" => &self.no_implicit_names, + "draft/persistence" => &self.persistence, + "draft/pre-away" => &self.pre_away, + "draft/read-marker" => &self.read_marker, + "draft/relaymsg" => &self.relaymsg, + "extended-join" => &self.extended_join, + "extended-monitor" => &self.extended_monitor, + "invite-notify" => &self.invite_notify, + "labeled-response" => &self.labeled_response, + "multi-prefix" => &self.multi_prefix, + "setname" => &self.setname, + "standard-replies" => &self.standard_replies, + "tls" => &self.tls, + "userhost-in-names" => &self.userhost_in_names, + _ => unimplemented!("cap: {cap}"), + } + } pub fn set_from_name(&mut self, cap: &str, enabled: Option) { #[allow(clippy::option_map_unit_fn)] match cap { @@ -247,7 +292,7 @@ impl Capabilities { self.languages.has = true; enabled.map(|e| self.languages.enabled = e); } - "draft/no-implicit-names" => { + "no-implicit-names" | "draft/no-implicit-names" => { self.no_implicit_names.has = true; enabled.map(|e| self.no_implicit_names.enabled = e); } @@ -267,10 +312,6 @@ impl Capabilities { self.relaymsg.has = true; enabled.map(|e| self.relaymsg.enabled = e); } - "ergo.chat/nope" => { - self.ergo_nope.has = true; - enabled.map(|e| self.ergo_nope.enabled = e); - } "extended-join" => { self.extended_join.has = true; enabled.map(|e| self.extended_join.enabled = e); @@ -299,11 +340,19 @@ impl Capabilities { self.standard_replies.has = true; enabled.map(|e| self.standard_replies.enabled = e); } + "tls" => { + self.tls.has = true; + enabled.map(|e| self.tls.enabled = e); + } "userhost-in-names" => { self.userhost_in_names.has = true; enabled.map(|e| self.userhost_in_names.enabled = e); } - _ if cap.starts_with("soju.im") || cap.starts_with("znc.in") => (), + _ if cap.starts_with("soju.im") + || cap.starts_with("znc.in") + || cap.starts_with("inspircd.org") + || cap.starts_with("ergo.chat") + || cap.starts_with("solanum.chat") => {} _ => unimplemented!("cap: {cap}"), }; } @@ -311,47 +360,78 @@ impl Capabilities { #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Support { + pub accept: Option, + pub account_extended_ban: Option>, pub away_length: Option, pub bot: Option, + pub caller_id: Option, pub case_mapping: Option, pub channel_limit: Option>, pub channel_modes: Option>, pub channel_length: Option, pub channel_types: Option, pub chat_history: Option, + pub client_tag_deny: Option>, + pub deaf: Option, // search extensions for list command pub elist: Option, + pub esilence: Option, + pub etrace: bool, pub excepts: bool, pub extban: Option, + pub host_length: Option, + pub fnc: bool, pub forward: Option, pub invex: bool, + pub key_length: Option, + pub knock: bool, pub kick_length: Option, + pub line_length: Option, pub max_list: Option>, pub max_targets: Option, - pub modes: bool, + pub modes: Option, pub monitor: Option, + pub name_length: Option, + pub namesx: bool, pub message_ref_types: Option>, + pub max_nick_length: Option, pub network: Option, pub nick_length: Option, pub prefix: Option>, pub rp_channel: Option, pub rp_user: Option, + pub remove: bool, pub safe_list: bool, pub safe_rate: bool, + pub secure_list: Option, + pub silence: Option, pub status_message: Option, pub target_max: Option)>>, pub topic_length: Option, + pub uhnames: bool, + pub user_ip: bool, + pub user_length: Option, + pub user_modes: Option>, pub utf8_mapping: Option, pub utf8_only: bool, pub vapid: Option, + pub vbanlist: bool, + pub vlist: Option, + pub watch: Option, pub whox: bool, } impl Support { pub fn set(&mut self, key: &str, value: Option<&str>) { match key.trim() { + "ACCEPT" => self.accept = value.map(|v| i64::from_str(v).unwrap()), + "ACCOUNTEXTBAN" => { + self.account_extended_ban = + value.map(|v| v.split(',').map(ToOwned::to_owned).collect()) + } "AWAYLEN" => self.away_length = value.map(|v| i64::from_str(v).unwrap()), "BOT" => self.bot = value.map(|v| v.chars().nth(0).unwrap()), + "CALLERID" => self.caller_id = value.map(|v| v.chars().nth(0).unwrap_or('g')), "CASEMAPPING" => self.case_mapping = value.map(ToOwned::to_owned), "CHANLIMIT" => { self.channel_limit = value.map(|v| { @@ -374,12 +454,23 @@ impl Support { "draft/CHATHISTORY" | "CHATHISTORY" => { self.chat_history = value.map(|v| i64::from_str(v).unwrap()) } + "CLIENTTAGDENY" => { + self.client_tag_deny = value.map(|v| v.split(',').map(ToOwned::to_owned).collect()) + } + "DEAF" => self.deaf = value.map(|v| v.chars().nth(0).unwrap()), "ELIST" => self.elist = value.map(ToOwned::to_owned), + "ESILENCE" => self.esilence = value.map(ToOwned::to_owned), + "ETRACE" => self.etrace = true, "EXCEPTS" => self.excepts = true, "EXTBAN" => self.extban = value.map(ToOwned::to_owned), + "HOSTLEN" => self.host_length = value.map(|v| i64::from_str(v).unwrap()), + "FNC" => self.fnc = true, "FORWARD" => self.forward = value.map(ToOwned::to_owned), "INVEX" => self.invex = true, + "KEYLEN" => self.key_length = value.map(|v| i64::from_str(v).unwrap()), + "KNOCK" => self.knock = true, "KICKLEN" => self.kick_length = value.map(|v| i64::from_str(v).unwrap()), + "LINELEN" => self.line_length = value.map(|v| i64::from_str(v).unwrap()), "MAXLIST" => { self.max_list = value.map(|v| { v.split(',') @@ -392,12 +483,15 @@ impl Support { }) } "MAXTARGETS" => self.max_targets = value.map(|v| i64::from_str(v).unwrap()), - "MODES" => self.modes = true, + "MODES" => self.modes = value.map(|v| i64::from_str(v).unwrap()), "MONITOR" => self.monitor = value.map(|v| i64::from_str(v).unwrap()), + "NAMELEN" => self.name_length = value.map(|v| i64::from_str(v).unwrap()), + "NAMESX" => self.namesx = true, "MSGREFTYPES" => { self.message_ref_types = value.map(|v| v.split(',').map(ToOwned::to_owned).collect()) } + "MAXNICKLEN" => self.max_nick_length = value.map(|v| i64::from_str(v).unwrap()), "NETWORK" => self.network = value.map(ToOwned::to_owned), "NICKLEN" => self.nick_length = value.map(|v| i64::from_str(v).unwrap()), "PREFIX" => { @@ -410,8 +504,11 @@ impl Support { } "RPCHAN" => self.rp_channel = value.map(|v| v.chars().nth(0).unwrap()), "RPUSER" => self.rp_user = value.map(|v| v.chars().nth(0).unwrap()), + "REMOVE" => self.remove = true, "SAFELIST" => self.safe_list = true, "SAFERATE" => self.safe_rate = true, + "SECURELIST" => self.secure_list = value.map(|v| i64::from_str(v).unwrap()), + "SILENCE" => self.silence = value.map(|v| i64::from_str(v).unwrap()), "STATUSMSG" => self.status_message = value.map(ToOwned::to_owned), "TARGMAX" => { self.target_max = value.map(|v| { @@ -425,11 +522,20 @@ impl Support { }) } "TOPICLEN" => self.topic_length = value.map(|v| i64::from_str(v).unwrap()), + "UHNAMES" => self.uhnames = true, + "USERIP" => self.user_ip = true, + "USERLEN" => self.user_length = value.map(|v| i64::from_str(v).unwrap()), + "USERMODES" => { + self.user_modes = value.map(|v| v.split(',').map(ToOwned::to_owned).collect()) + } "UTF8MAPPING" => self.utf8_mapping = value.map(ToOwned::to_owned), "UTF8ONLY" => self.utf8_only = true, "VAPID" => self.vapid = value.map(ToOwned::to_owned), + "VBANLIST" => self.vbanlist = true, + "VLIST" => self.vlist = value.map(ToOwned::to_owned), + "WATCH" => self.watch = value.map(|v| i64::from_str(v).unwrap()), "WHOX" => self.whox = true, - _ => unimplemented!("isupport: {key}, {value:?}"), + _ => debug!("ignored isupport: {key}, {value:?}"), }; } } @@ -518,7 +624,7 @@ pub enum MessageType { Quit, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct TextMessage { pub content: String, pub reactions: HashMap>, @@ -579,7 +685,7 @@ pub enum ServerEvent { #[derive(Debug, Clone, PartialEq, Eq)] pub struct History { - pub channel: String, + pub target: String, pub messages: Vec, } @@ -598,6 +704,9 @@ pub enum OrbitError { #[error("{0}")] SaslFailed(String), + #[error("Capability '{0}' is not enabled on this server")] + CapabilityDisabled(&'static str), + #[error("{0}")] Generic(String), diff --git a/packages/core/core-wasm/src/database.rs b/packages/core/core-wasm/src/database.rs index f926708..1bef2ba 100644 --- a/packages/core/core-wasm/src/database.rs +++ b/packages/core/core-wasm/src/database.rs @@ -10,6 +10,7 @@ use indexed_db_futures::{ use indexed_db_futures::{BuildSerde, KeyRange}; use serde::{Deserialize, Serialize}; +#[allow(unused_imports)] use crate::dbg; const MESSAGE_STORE: &str = "messages"; diff --git a/packages/core/core-wasm/src/lib.rs b/packages/core/core-wasm/src/lib.rs index e7889db..e9a6588 100644 --- a/packages/core/core-wasm/src/lib.rs +++ b/packages/core/core-wasm/src/lib.rs @@ -50,6 +50,16 @@ macro_rules! dbg { }; } +macro_rules! cmd_resp { + ($e:expr, $p:path) => { + match $e { + $p(value) => Ok(value), + CommandResponse::Error(e) => Err(e), + _ => unreachable!("expected {}, got: {:?}", stringify!($p), $e), + } + }; +} + use tracing_subscriber::prelude::*; use tracing_subscriber_wasm::MakeConsoleWriter; @@ -146,9 +156,7 @@ impl IrcConnection { .context("Failed to send ActorMessage")?; let resp = rx.await.context("Failed to await actor state message")?; - let CommandResponse::GetState(server) = resp else { - unreachable!("expected state, got: {:?}", resp); - }; + let server = cmd_resp!(resp, CommandResponse::GetState)?; Ok((*server).into()) } @@ -262,11 +270,9 @@ impl IrcConnection { .context("Failed to send ActorMessage")?; let resp = rx.await.context("Failed to await actor sign in message")?; - let CommandResponse::SignIn(result) = resp else { - unreachable!("expected sign in, got: {:?}", resp); - }; + let result = cmd_resp!(resp, CommandResponse::SignIn)?; - Ok(result?) + Ok(result) } #[wasm_bindgen] @@ -291,11 +297,9 @@ impl IrcConnection { let resp = rx.await.context("Failed to await actor sign in message")?; - let CommandResponse::SignIn(result) = resp else { - unreachable!("expected sign in, got: {:?}", resp); - }; + let result = cmd_resp!(resp, CommandResponse::SignIn)?; - Ok(result?) + Ok(result) } #[wasm_bindgen] @@ -314,12 +318,10 @@ impl IrcConnection { .context("Failed to send ActorMessage")?; let resp = rx.await.context("Failed to await actor join message")?; - let CommandResponse::Join(name) = resp else { - unreachable!("expected join, got: {:?}", resp); - }; + let channel = cmd_resp!(resp, CommandResponse::Join)?; Ok(IrcChannel { - name, + name: channel.metadata.name, address: self.address.clone(), }) } @@ -343,9 +345,7 @@ impl IrcConnection { .context("Failed to send ActorMessage")?; let resp = rx.await.context("Failed to await actor history message")?; - let CommandResponse::History(history) = resp else { - unreachable!("expected history, got: {:?}", resp); - }; + let history = cmd_resp!(resp, CommandResponse::History)?; Ok(history.into()) } @@ -371,9 +371,7 @@ impl IrcChannel { .context("Failed to send ActorMessage")?; let resp = rx.await.context("Failed to await actor state message")?; - let CommandResponse::GetChannelState(channel) = resp else { - unreachable!("expected state, got: {:?}", resp); - }; + let channel = cmd_resp!(resp, CommandResponse::GetChannelState)?; Ok((*channel).map(Into::into)) } @@ -392,10 +390,8 @@ impl IrcChannel { .await .context("Failed to send ActorMessage")?; - let resp = rx.await.context("Failed to await actor message")?; - let CommandResponse::Privmsg(message) = resp else { - unreachable!("expected privmsg, got: {:?}", resp); - }; + let resp = rx.await.context("Failed to await actor privmessage")?; + let message = cmd_resp!(resp, CommandResponse::Privmsg)?; Ok((*message).into()) } @@ -453,7 +449,7 @@ impl SendCommand for OutgoingSink { type Error = WebSocketError; async fn message(&mut self, message: irc_proto::Message) -> Result<(), Self::Error> { self.inner - .send(websocket::Message::Text(message.to_string())) + .send(websocket::Message::Text(dbg!(message).to_string())) .await?; Ok(()) @@ -630,6 +626,7 @@ impl From for OrbitError { let kind = match error { state::OrbitError::NickTaken => OrbitErrorKind::NickTaken, state::OrbitError::SaslFailed(_) => OrbitErrorKind::SaslFailed, + state::OrbitError::CapabilityDisabled(_) => OrbitErrorKind::CapabilityDisabled, state::OrbitError::Generic(_) => OrbitErrorKind::Generic, state::OrbitError::Unknown(_) => OrbitErrorKind::Unknown, }; @@ -646,6 +643,7 @@ impl From for OrbitError { pub enum OrbitErrorKind { NickTaken, SaslFailed, + CapabilityDisabled, Generic, Unknown, } @@ -678,7 +676,7 @@ pub struct History { impl From for History { fn from(history: state::History) -> Self { Self { - channel: history.channel, + channel: history.target, messages: history.messages.into_iter().map(Into::into).collect(), } }