From 6c91669a4c0f5c1de920b4a3ef711d309cf65a6c Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 7 Jan 2023 20:14:23 +0000 Subject: [PATCH] Implement TOPIC command --- Cargo.lock | 261 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/channel.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- src/channel/response.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++++----------- src/client.rs | 36 ++++++++++++++++++++++++++++++++---- src/messages.rs | 17 +++++++++++++++++ src/server.rs | 1 + 7 files changed, 416 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5872ae3..8acb4e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,6 +59,15 @@ dependencies = [ ] [[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] name = "anyhow" version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -77,18 +86,61 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] name = "bytes" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" + +[[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] name = "crossbeam-channel" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -108,6 +160,50 @@ dependencies = [ ] [[package]] +name = "cxx" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "either" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -276,6 +372,30 @@ dependencies = [ ] [[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] name = "irc-proto" version = "0.15.0" source = "git+https://github.com/JordanForks/irc#923e080f2408ae301135e82549cff80e92aaa844" @@ -303,6 +423,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" [[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -315,6 +444,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] name = "lock_api" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -356,7 +494,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -371,6 +509,25 @@ dependencies = [ ] [[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] name = "num_cpus" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -491,6 +648,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] name = "serde" version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -562,6 +725,15 @@ dependencies = [ ] [[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] name = "thiserror" version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -591,12 +763,24 @@ dependencies = [ ] [[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] name = "titanircd" version = "0.1.0" dependencies = [ "actix", "actix-rt", "anyhow", + "chrono", "futures", "irc-proto", "itertools", @@ -745,6 +929,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -752,11 +942,71 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -773,6 +1023,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index 497c3dc..9cfaf7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" actix = "0.13" actix-rt = "2.7" anyhow = "1.0" +chrono = "0.4" futures = "0.3" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } diff --git a/src/channel.rs b/src/channel.rs index 375b9e7..f6f9249 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -3,16 +3,17 @@ pub mod response; use std::collections::HashMap; use actix::{Actor, Addr, AsyncContext, Context, Handler, MessageResult}; +use chrono::{DateTime, Utc}; use irc_proto::Command; -use tracing::{error, info, instrument, Span}; +use tracing::{debug, error, info, instrument, Span}; use crate::{ channel::response::{ChannelNamesList, ChannelTopic}, client::Client, connection::InitiatedConnection, messages::{ - Broadcast, ChannelJoin, ChannelList, ChannelMessage, ChannelPart, ServerDisconnect, - UserNickChange, + Broadcast, ChannelFetchTopic, ChannelJoin, ChannelList, ChannelMessage, ChannelPart, + ChannelUpdateTopic, ServerDisconnect, UserNickChange, }, }; @@ -21,6 +22,7 @@ use crate::{ pub struct Channel { pub name: String, pub clients: HashMap, InitiatedConnection>, + pub topic: Option, } impl Actor for Channel { @@ -84,7 +86,7 @@ impl Handler for Channel { } } -/// Recieved when a user changes their nick. +/// Received when a user changes their nick. impl Handler for Channel { type Result = (); @@ -128,10 +130,12 @@ impl Handler for Channel { } // send the channel's topic to the joining user - msg.client.do_send(Broadcast { - message: ChannelTopic::new(self).into_message(self.name.to_string()), - span: Span::current(), - }); + for message in ChannelTopic::new(self).into_messages(self.name.to_string(), true) { + msg.client.do_send(Broadcast { + message, + span: Span::current(), + }); + } // send the user list to the user for message in ChannelNamesList::new(self).into_messages(msg.connection.nick.to_string()) { @@ -145,6 +149,46 @@ impl Handler for Channel { } } +/// Updates the channel topic, as requested by a connected user. +impl Handler for Channel { + type Result = (); + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: ChannelUpdateTopic, _ctx: &mut Self::Context) -> Self::Result { + let Some(client_info) = self.clients.get(&msg.client) else { + return; + }; + + debug!(msg.topic, "User is attempting to update channel topic"); + + self.topic = Some(CurrentChannelTopic { + topic: msg.topic, + set_by: client_info.nick.to_string(), + set_time: Utc::now(), + }); + + for (client, connection) in &self.clients { + for message in ChannelTopic::new(self).into_messages(connection.nick.to_string(), false) + { + client.do_send(Broadcast { + message, + span: Span::current(), + }); + } + } + } +} + +/// Returns the current channel topic to the user. +impl Handler for Channel { + type Result = MessageResult; + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: ChannelFetchTopic, _ctx: &mut Self::Context) -> Self::Result { + MessageResult(ChannelTopic::new(self)) + } +} + /// Received when a client is parting the channel and broadcasts it to all connected users. impl Handler for Channel { type Result = (); @@ -194,3 +238,10 @@ impl Handler for Channel { ctx.notify(message); } } + +#[derive(Clone)] +pub struct CurrentChannelTopic { + pub topic: String, + pub set_by: String, + pub set_time: DateTime, +} diff --git a/src/channel/response.rs b/src/channel/response.rs index 087e854..0389e20 100644 --- a/src/channel/response.rs +++ b/src/channel/response.rs @@ -1,10 +1,13 @@ use irc_proto::{Command, Message, Prefix, Response}; -use crate::{channel::Channel, SERVER_NAME}; +use crate::{ + channel::{Channel, CurrentChannelTopic}, + SERVER_NAME, +}; pub struct ChannelTopic { channel_name: String, - topic: String, + topic: Option, } impl ChannelTopic { @@ -12,19 +15,51 @@ impl ChannelTopic { pub fn new(channel: &Channel) -> Self { Self { channel_name: channel.name.to_string(), - topic: "hello world!".to_string(), + topic: channel.topic.clone(), } } #[must_use] - pub fn into_message(self, for_user: String) -> Message { - Message { - tags: None, - prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), - command: Command::Response( - Response::RPL_TOPIC, - vec![for_user, self.channel_name, self.topic], - ), + pub fn into_messages(self, for_user: String, skip_on_none: bool) -> Vec { + if let Some(topic) = self.topic { + vec![ + Message { + tags: None, + prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), + command: Command::Response( + Response::RPL_TOPIC, + vec![ + for_user.to_string(), + self.channel_name.to_string(), + topic.topic, + ], + ), + }, + Message { + tags: None, + prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), + command: Command::Response( + Response::RPL_TOPICWHOTIME, + vec![ + for_user, + self.channel_name.to_string(), + topic.set_by, + topic.set_time.timestamp().to_string(), + ], + ), + }, + ] + } else if !skip_on_none { + vec![Message { + tags: None, + prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), + command: Command::Response( + Response::RPL_NOTOPIC, + vec![for_user, self.channel_name, "No topic is set".to_string()], + ), + }] + } else { + vec![] } } } diff --git a/src/client.rs b/src/client.rs index 14c6d1d..8ade8d7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, time::Duration}; use actix::{ fut::wrap_future, io::WriteHandler, Actor, ActorContext, ActorFutureExt, Addr, AsyncContext, - Context, Handler, MessageResult, ResponseActFuture, Running, StreamHandler, + Context, Handler, MessageResult, ResponseActFuture, Running, StreamHandler, WrapFuture, }; use futures::FutureExt; use irc_proto::{error::ProtocolError, ChannelExt, Command, Message}; @@ -13,8 +13,8 @@ use crate::{ channel::Channel, connection::{InitiatedConnection, MessageSink}, messages::{ - Broadcast, ChannelJoin, ChannelList, ChannelMessage, ChannelPart, FetchClientDetails, - ServerDisconnect, UserNickChange, + Broadcast, ChannelFetchTopic, ChannelJoin, ChannelList, ChannelMessage, ChannelPart, + ChannelUpdateTopic, FetchClientDetails, ServerDisconnect, UserNickChange, }, server::Server, SERVER_NAME, @@ -324,7 +324,35 @@ impl StreamHandler> for Client { }); } Command::ChannelMODE(_, _) => {} - Command::TOPIC(_, _) => {} + Command::TOPIC(channel, topic) => { + let Some(channel) = self.channels.get(&channel) else { + return; + }; + + #[allow(clippy::option_if_let_else)] + if let Some(topic) = topic { + channel.do_send(ChannelUpdateTopic { + topic, + client: ctx.address(), + span: Span::current(), + }); + } else { + let span = Span::current(); + let fut = channel + .send(ChannelFetchTopic { span }) + .into_actor(self) + .map(|result, this, _ctx| { + for message in result + .unwrap() + .into_messages(this.connection.nick.to_string(), false) + { + this.writer.write(message); + } + }); + + ctx.spawn(fut); + } + } Command::NAMES(channel_names, _) => { // split the list of channel names... let channels = parse_channel_name_list(channel_names.as_deref().unwrap_or("")); diff --git a/src/messages.rs b/src/messages.rs index 7fb809e..ae0a330 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -59,6 +59,23 @@ pub struct ChannelList { pub span: Span, } +/// Retrieves the current channel topic. +#[derive(Message)] +#[rtype(result = "super::channel::response::ChannelTopic")] +pub struct ChannelFetchTopic { + pub span: Span, +} + +/// Sent from a particular user to a channel when the user attempts to update the +/// topic. +#[derive(Message)] +#[rtype(result = "()")] +pub struct ChannelUpdateTopic { + pub topic: String, + pub client: Addr, + pub span: Span, +} + /// Sends a raw irc message to a channel/user. #[derive(Message, Clone)] #[rtype(result = "()")] diff --git a/src/server.rs b/src/server.rs index 27df3ac..1970a32 100644 --- a/src/server.rs +++ b/src/server.rs @@ -96,6 +96,7 @@ impl Handler for Server { Channel { name: msg.channel_name.clone(), clients: HashMap::new(), + topic: None, } .start() }) -- libgit2 1.7.2