Thumbnail

steew/belltoll.git

Clone URL: https://git.buni.party/steew/belltoll.git

commit 0174333c36996162472a117add1ecdf495a34092 Author: Daniel Pérez <steew@psi.my.domain> Date: Fri Jan 09 17:13:52 2026 +0000 Implemented replies and username color hashing diff --git a/Cargo.lock b/Cargo.lock index 4f1b3a2..9214729 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -646 +647 @@ dependencies = [   "irc",   "rust-ini",   "serenity", + "sha1",   "tokio",  ]   diff --git a/Cargo.toml b/Cargo.toml index ff2260e..966af60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94 +95 @@ futures-util = "0.3.31"  irc = "1.1.0"  rust-ini = "0.21.3"  serenity = "0.12.5" +sha1 = "0.10.6"  tokio = { version = "1.49.0", features = ["rt-multi-thread"] } diff --git a/src/main.rs b/src/main.rs index 8509a0e..0e04c27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18 +111 @@ +use std::collections::HashMap;  use std::str::FromStr;  use std::sync::Arc;   +use futures_util::StreamExt;  use ini::Ini;  use irc::client::data::Config; +use irc::proto::Command;  use serenity::all::ChannelId;  use serenity::all::Webhook;  use serenity::prelude::*; @@ -476 +509 @@ async fn main() { // shared http sender for the discord client let shared_http = discord_client.http.clone(); + let shared_cache = discord_client.cache.clone(); + + let avatars: HashMap<String, String> = HashMap::new(); // spawn discord client async thread let _client_handle = spawn(async move { @@ -5523 +616 @@ async fn main() { } });   - // TODO: remove and read from file - // ====================================================================================== - // IRC initialization - let config = Config { - nickname: Some("belltoll".to_owned()), - server: Some("irc.libera.chat".to_owned()), - channels: vec!["##steew".to_owned()], - ..Default::default() - }; - - let irc_client = irc::client::Client::from_config(config).await.unwrap(); - irc_client.identify().unwrap(); - let irc_sender = irc_client.sender().clone(); - - let _irc_handle = spawn(async move { - irc_producer(irc_client, irc_buffer_reference, irc_notify).await; - }); // Configuration file read for channel bridge associations // ====================================================================================== let ini_conf_file = "config.ini"; @@ -867 +759 @@ async fn main() { let irc_chan = String::from_str(i_channel).expect("Channel name is not valid! {i_channel}"); let mut ircs = Vec::new(); ircs.push(irc_chan); - assoc.bridge_assoc.insert(ChannelId::new(chid), ircs.clone()); + let chid = ChannelId::new(chid); + assoc.bridge_assoc.insert(chid, ircs.clone()); + // ========================= } }, None => { panic!("Expected an [assoc] section with bridge associations.") }, @@ -1086 +9931 @@ async fn main() { println!("{:?}", assoc.bridge_assoc.values()); println!("Webhooks:"); println!("{:?}", assoc.chid_webhook_assoc.keys()); + println!("Avatar URLs:"); + println!("{:?}", avatars.values()); + + // ====================================================================================== + // IRC initialization + let config = Config { + nickname: Some("belltoll".to_owned()), + server: Some("irc.libera.chat".to_owned()), + channels: vec!["##steew".to_owned()], + ..Default::default() + }; + + let irc_client = irc::client::Client::from_config(config).await.unwrap(); + irc_client.identify().unwrap(); + let irc_sender = irc_client.sender().clone(); + + for v in assoc.bridge_assoc.values() { + for c in v.iter() { + irc_client.send_join(c).expect("Could not join channel {c}"); + } + } + + let _irc_handle = spawn(async move { + irc_producer(irc_client, irc_buffer_reference, irc_notify).await; + });   // ====================================================================================== // Relay consumer thread spawn @@ -1177 +1338 @@ async fn main() { notify, shared_http, irc_sender, - assoc + assoc, + avatars ) .await; }); diff --git a/src/relay.rs b/src/relay.rs index ab7048c..f779a65 100644 --- a/src/relay.rs +++ b/src/relay.rs @@ -311 +314 @@ use std::collections::VecDeque;  use std::sync::Arc;    use irc::client::Sender; +use serenity::all::Cache;  use serenity::all::ChannelId;  use serenity::all::ExecuteWebhook;  use serenity::all::Http;  use serenity::all::Webhook; +use serenity::builder;  use serenity::prelude::*; +use sha1::{Sha1, Digest};  use tokio::sync::Notify;    #[derive(Debug)] @@ -1710 +2019 @@ pub enum RelayDirection { DIS2IRC(ChannelId),  }   +pub enum MessageType { + Normal, + // discord reply, containing the u64 message id that it references, and the author name. + ReplyDiscord((String, String)), + // irc reply, containing the message base64 hash it references. + ReplyIrc(String) +} +  pub struct RelayMessage { pub contents: String, pub direction: RelayDirection, - pub author: String + pub author: String, + pub message_type: MessageType  }    pub struct MessageBuffer { @@ -827 +948 @@ impl Default for RelayMessage { RelayMessage { contents: String::new(), direction: RelayDirection::INVALID, - author: String::new() + author: String::new(), + message_type: MessageType::Normal } }  } @@ -10313 +11633 @@ impl TypeMapKey for RelayNotify { type Value = Arc<RelayNotify>;  }   +fn format_name(name: &str) -> String { + // XMPP RFC 3174 implementation + // get hash + let mut hasher = Sha1::new(); + hasher.update(name); + let result = hasher.finalize(); + // obtain the last 16 bits + let last16 = result.last_chunk::<2>().unwrap(); + let last16_float = f32::from(((last16[0] as u16) << 8) | (last16[1] as u16)); + let hue = last16_float * 360.0 / 65535.0; + let hue = hue.ceil(); + let hue: u32 = hue.to_bits() % 87; + println!("{hue}"); + // insert invisible character to prevent pings + let (split_name_first, split_name_rest) = name.split_at(1); + + format!("\x03{hue:02}{split_name_first}​{split_name_rest}\x03") +} +    pub async fn relay_consumer( buffer: Arc<RwLock<MessageBuffer>>, notify: Arc<RelayNotify>, http: Arc<Http>, sender: Sender, - assoc: RelayAssoc + assoc: RelayAssoc, + avatars: HashMap<String, String>  ) { loop { // await for new relay pending events @@ -13131 +16445 @@ pub async fn relay_consumer( match target { RelayDirection::DIS2IRC(t) => { let webhook = Webhook::from_url(&http, assoc.chid_webhook_assoc.get(&t).expect("Expected a webhook url for channel {t.get()}")).await.unwrap(); - let builder = ExecuteWebhook::new().content(pending.contents).username(pending.author); + let builder: ExecuteWebhook; + let avatar_url: Option<String> = None; + if let Some(avatar_url) = avatar_url { + println!("{avatar_url}"); + builder = ExecuteWebhook::new().content(pending.contents).username(pending.author).avatar_url(avatar_url); + } else { + builder = ExecuteWebhook::new().content(pending.contents).username(pending.author); + } + webhook.execute(&http, false, builder).await.expect("Could not execute webhook."); }, _ => { panic!("Found no target to send the message to!") } } } RelayDirection::DIS2IRC(chan) => { - let unpingable_name = pending.author.clone(); - let (first, rest) = unpingable_name.split_at(1); - let mut unpingable_name = String::new(); - unpingable_name.push(char::from_u32(0x03).unwrap()); - unpingable_name.push_str("04"); - unpingable_name.push_str(first); - unpingable_name.push_str("​"); - unpingable_name.push_str(rest); - unpingable_name.push(char::from_u32(0x03).unwrap()); - + let source_name = format_name(&pending.author); let target = assoc.find_target(RelayDirection::DIS2IRC(chan)); - match target { RelayDirection::IRC2DIS(t) => { - let response = format!("<{}>: {}", unpingable_name, pending.contents); + let response; + let message_contents = pending.contents; + match pending.message_type { + MessageType::ReplyDiscord(tuple) => { + let target_reply_user = format_name(&tuple.1); + let origin_contents = tuple.0; + response = format!("<{source_name} replying to: {target_reply_user}> \"{origin_contents}\"\r\n\t↪ {message_contents}"); + }, + MessageType::Normal => { + response = format!("<{source_name}> {message_contents}"); + }, + _ => { + // found invalid message type here!! + println!("Invalid reply type found."); + return; + } + } sender.send_privmsg(t, response).unwrap(); }, - _ => { panic!("Found no target to send the message to!") } + _ => { println!("Found no target to send the message to!") } } } _ => {} diff --git a/src/relay_discord.rs b/src/relay_discord.rs index 1afabb2..00ea812 100644 --- a/src/relay_discord.rs +++ b/src/relay_discord.rs @@ -13 +15 @@ +use std::hash::Hash; +  use serenity::all::Message;  use serenity::all::Ready;  use serenity::async_trait; @@ -147 +169 @@ impl EventHandler for Handler { let data = ctx.data.read().await; let buffer_lock = data.get::<MessageBuffer>().unwrap().clone();   - if msg.author.bot { + println!("author id: {}, cache id: {}", msg.author.name, ctx.cache.current_user().name); + if let Some(_) = msg.webhook_id { + println!("Same discord author as the bot"); return; };   @@ -2510 +2913 @@ impl EventHandler for Handler { new_message.contents = msg.content; new_message.direction = RelayDirection::DIS2IRC(msg.channel_id); new_message.author = msg.author.name; + // check if message is a reply to also relay it + if let Some(reply) = msg.referenced_message { + new_message.message_type = MessageType::ReplyDiscord((reply.content, reply.author.name)); + } // push the pending message to the relay buffer relay_buffer.pending_relay_messages.push_back(new_message); } - println!("Added discord message to buffer."); { let notify = data.get::<RelayNotify>().unwrap().clone(); notify.notify.notify_one(); diff --git a/src/relay_irc.rs b/src/relay_irc.rs index 2129a0f..3eb8c07 100644 --- a/src/relay_irc.rs +++ b/src/relay_irc.rs @@ -197 +1913 @@ pub async fn irc_producer( match message.command { Command::PRIVMSG(_, contents) => { { + println!("IRC!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); let uname = msg_clone.source_nickname().unwrap(); + println!("username: {uname}, current nick: {}", irc_client.current_nickname()); + if uname.eq_ignore_ascii_case(irc_client.current_nickname()) { + println!("Ignoring message from myself!"); + return; + }; let mut buffer = buffer_reference.write().await; let mut new_message = RelayMessage::default(); new_message.contents = contents;