Added documentation.
This commit is contained in:
24
Cargo.lock
generated
24
Cargo.lock
generated
@@ -180,9 +180,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.10.1"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cargo-husky"
|
name = "cargo-husky"
|
||||||
@@ -226,9 +226,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.51"
|
version = "4.5.53"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
|
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -236,9 +236,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.51"
|
version = "4.5.53"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
|
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -824,9 +824,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "genai"
|
name = "genai"
|
||||||
version = "0.4.3"
|
version = "0.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48317c8c4a7011ffb748502f9c45408a351103ad225f26825d84f2ff0ac49b25"
|
checksum = "814c33e79506556ecba6b5f8e39a2fe423262fd3903856377ad2ae6a857c6032"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"derive_more 2.0.1",
|
"derive_more 2.0.1",
|
||||||
@@ -2440,9 +2440,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "3.15.1"
|
version = "3.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04"
|
checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -2459,9 +2459,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with_macros"
|
name = "serde_with_macros"
|
||||||
version = "3.15.1"
|
version = "3.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955"
|
checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|||||||
28
src/chat.rs
28
src/chat.rs
@@ -1,3 +1,8 @@
|
|||||||
|
//! Handles interaction with IRC.
|
||||||
|
//!
|
||||||
|
//! Each instance of [`Chat`] handles a single connection to an IRC
|
||||||
|
//! server.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use color_eyre::{Result, eyre::WrapErr};
|
use color_eyre::{Result, eyre::WrapErr};
|
||||||
@@ -9,20 +14,27 @@ use tracing::{Level, event, instrument};
|
|||||||
|
|
||||||
use crate::{Event, EventManager, LLMHandle, plugin};
|
use crate::{Event, EventManager, LLMHandle, plugin};
|
||||||
|
|
||||||
|
/// Chat struct that is used to interact with IRC chat.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Chat {
|
pub struct Chat {
|
||||||
|
/// The actual IRC [`irc::client`](client).
|
||||||
client: Client,
|
client: Client,
|
||||||
|
/// Event manager for handling plugin interaction.
|
||||||
event_manager: Arc<EventManager>,
|
event_manager: Arc<EventManager>,
|
||||||
|
/// Handle for whichever LLM is being used.
|
||||||
llm_handle: LLMHandle, // FIXME: This needs to be thread safe, and shared, etc.
|
llm_handle: LLMHandle, // FIXME: This needs to be thread safe, and shared, etc.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need: owners, channels, username, nick, server, password
|
impl Chat {
|
||||||
#[instrument]
|
// Need: owners, channels, username, nick, server, password rather than reading
|
||||||
pub async fn new(
|
// the config values directly.
|
||||||
|
/// Creates a new [`Chat`].
|
||||||
|
#[instrument]
|
||||||
|
pub async fn new(
|
||||||
settings: &MainConfig,
|
settings: &MainConfig,
|
||||||
handle: &LLMHandle,
|
handle: &LLMHandle,
|
||||||
manager: Arc<EventManager>,
|
manager: Arc<EventManager>,
|
||||||
) -> Result<Chat> {
|
) -> Result<Chat> {
|
||||||
// Going to just assign and let the irc library handle errors for now, and
|
// Going to just assign and let the irc library handle errors for now, and
|
||||||
// add my own checking if necessary.
|
// add my own checking if necessary.
|
||||||
let port: u16 = settings.get("port")?;
|
let port: u16 = settings.get("port")?;
|
||||||
@@ -47,10 +59,10 @@ pub async fn new(
|
|||||||
llm_handle: handle.clone(),
|
llm_handle: handle.clone(),
|
||||||
event_manager: manager,
|
event_manager: manager,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chat {
|
/// Drives the event loop for the chat.
|
||||||
pub async fn run(&mut self, mut command_in: mpsc::Receiver<plugin::Plugin>) -> Result<()> {
|
pub async fn run(&mut self, mut command_in: mpsc::Receiver<plugin::PluginMsg>) -> Result<()> {
|
||||||
self.client.identify()?;
|
self.client.identify()?;
|
||||||
|
|
||||||
let mut stream = self.client.stream()?;
|
let mut stream = self.client.stream()?;
|
||||||
@@ -69,7 +81,7 @@ impl Chat {
|
|||||||
command = command_in.recv() => {
|
command = command_in.recv() => {
|
||||||
event!(Level::INFO, "Received command {:#?}", command);
|
event!(Level::INFO, "Received command {:#?}", command);
|
||||||
match command {
|
match command {
|
||||||
Some(plugin::Plugin::SendMessage {channel, message} ) => {
|
Some(plugin::PluginMsg::SendMessage {channel, message} ) => {
|
||||||
// Now to pass on the message.
|
// Now to pass on the message.
|
||||||
event!(Level::INFO, "Trying to send to channel.");
|
event!(Level::INFO, "Trying to send to channel.");
|
||||||
self.client.send_privmsg(&channel, &message).wrap_err("Couldn't send to channel")?;
|
self.client.send_privmsg(&channel, &message).wrap_err("Couldn't send to channel")?;
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
// Commands that are associated with external processes (commands).
|
//! Commands that are associated with external processes (commands).
|
||||||
|
//!
|
||||||
|
//! Process based plugins are just an assortment of executable files in
|
||||||
|
//! a provided directory. They are given arguments, and the response from
|
||||||
|
//! them is expected on stdout.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@@ -10,12 +14,14 @@ use color_eyre::{Result, eyre::eyre};
|
|||||||
use tokio::{fs::try_exists, process::Command, time::timeout};
|
use tokio::{fs::try_exists, process::Command, time::timeout};
|
||||||
use tracing::{Level, event};
|
use tracing::{Level, event};
|
||||||
|
|
||||||
|
/// Handle containing information about the directory containing commands.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CommandDir {
|
pub struct CommandDir {
|
||||||
command_path: PathBuf,
|
command_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandDir {
|
impl CommandDir {
|
||||||
|
/// Register a path containing commands.
|
||||||
pub fn new(command_path: impl AsRef<Path>) -> Self {
|
pub fn new(command_path: impl AsRef<Path>) -> Self {
|
||||||
event!(
|
event!(
|
||||||
Level::INFO,
|
Level::INFO,
|
||||||
@@ -27,6 +33,7 @@ impl CommandDir {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Look for a command. If it exists Ok(path) is returned.
|
||||||
async fn find_command(&self, name: impl AsRef<Path>) -> Result<String> {
|
async fn find_command(&self, name: impl AsRef<Path>) -> Result<String> {
|
||||||
let path = self.command_path.join(name.as_ref());
|
let path = self.command_path.join(name.as_ref());
|
||||||
|
|
||||||
@@ -43,6 +50,8 @@ impl CommandDir {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run the given [`command_name`]. It should exist in the directory provided as
|
||||||
|
/// the command_path.
|
||||||
pub async fn run_command(
|
pub async fn run_command(
|
||||||
&self,
|
&self,
|
||||||
command_name: impl AsRef<str>,
|
command_name: impl AsRef<str>,
|
||||||
@@ -65,6 +74,7 @@ impl CommandDir {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`run_command`] but with a timeout.
|
||||||
pub async fn run_command_with_timeout(
|
pub async fn run_command_with_timeout(
|
||||||
&self,
|
&self,
|
||||||
command_name: impl AsRef<str>,
|
command_name: impl AsRef<str>,
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
|
//! Internal representations of incoming events.
|
||||||
|
|
||||||
use irc::proto::{Command, Message};
|
use irc::proto::{Command, Message};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Represents an event. Probably from IRC.
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Event {
|
pub struct Event {
|
||||||
|
/// Who is the message from?
|
||||||
from: String,
|
from: String,
|
||||||
|
/// What is the message?
|
||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Event {
|
impl Event {
|
||||||
|
/// Creates a new message.
|
||||||
pub fn new(from: impl Into<String>, msg: impl Into<String>) -> Self {
|
pub fn new(from: impl Into<String>, msg: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
from: from.into(),
|
from: from.into(),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Handler for events to and from IPC, and process plugins.
|
||||||
|
|
||||||
use std::{collections::VecDeque, path::Path, sync::Arc};
|
use std::{collections::VecDeque, path::Path, sync::Arc};
|
||||||
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
@@ -9,12 +11,14 @@ use tokio::{
|
|||||||
};
|
};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{event::Event, plugin::Plugin};
|
use crate::{event::Event, plugin::PluginMsg};
|
||||||
|
|
||||||
// Hard coding for now. Maybe make this a parameter to new.
|
// Hard coding for now. Maybe make this a parameter to new.
|
||||||
const EVENT_BUF_MAX: usize = 1000;
|
const EVENT_BUF_MAX: usize = 1000;
|
||||||
|
|
||||||
// Manager for communication with plugins.
|
/// Manager for communication with plugins.
|
||||||
|
///
|
||||||
|
/// Keeps events in a ring buffer to track a certain amount of history.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EventManager {
|
pub struct EventManager {
|
||||||
announce: broadcast::Sender<String>, // Everything broadcasts here.
|
announce: broadcast::Sender<String>, // Everything broadcasts here.
|
||||||
@@ -22,6 +26,7 @@ pub struct EventManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EventManager {
|
impl EventManager {
|
||||||
|
/// Create a new [`EventManager``].
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
let (announce, _) = broadcast::channel(100);
|
let (announce, _) = broadcast::channel(100);
|
||||||
|
|
||||||
@@ -31,6 +36,7 @@ impl EventManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Broadcast an event to every subscribed listener.
|
||||||
pub async fn broadcast(&self, event: &Event) -> Result<()> {
|
pub async fn broadcast(&self, event: &Event) -> Result<()> {
|
||||||
let msg = serde_json::to_string(event)? + "\n";
|
let msg = serde_json::to_string(event)? + "\n";
|
||||||
|
|
||||||
@@ -49,7 +55,10 @@ impl EventManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NB: This assumes it has exclusive control of the FIFO.
|
// NB: This assumes it has exclusive control of the FIFO.
|
||||||
pub async fn start_fifo<P>(path: &P, command_tx: mpsc::Sender<Plugin>) -> Result<()>
|
/// Opens a fifo at [`path`]. This is where some plugins can send response events
|
||||||
|
/// to. The messages MUST be formatted in JSON and match one of the possible
|
||||||
|
/// [`PluginMsg`](plugin messages).
|
||||||
|
pub async fn start_fifo<P>(path: &P, command_tx: mpsc::Sender<PluginMsg>) -> Result<()>
|
||||||
where
|
where
|
||||||
P: AsRef<Path> + NixPath + ?Sized,
|
P: AsRef<Path> + NixPath + ?Sized,
|
||||||
{
|
{
|
||||||
@@ -65,7 +74,7 @@ impl EventManager {
|
|||||||
|
|
||||||
while reader.read_line(&mut line).await? > 0 {
|
while reader.read_line(&mut line).await? > 0 {
|
||||||
// Now handle the command.
|
// Now handle the command.
|
||||||
let cmd: Plugin = serde_json::from_str(&line)?;
|
let cmd: PluginMsg = serde_json::from_str(&line)?;
|
||||||
info!("Command received: {:?}.", cmd);
|
info!("Command received: {:?}.", cmd);
|
||||||
command_tx.send(cmd).await?;
|
command_tx.send(cmd).await?;
|
||||||
line.clear();
|
line.clear();
|
||||||
@@ -73,6 +82,8 @@ impl EventManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start a UNIX socket that will provide broadcast messages to any client that opens
|
||||||
|
/// the socket for listening.
|
||||||
pub async fn start_listening(self: Arc<Self>, broadcast_path: impl AsRef<Path>) {
|
pub async fn start_listening(self: Arc<Self>, broadcast_path: impl AsRef<Path>) {
|
||||||
let listener = UnixListener::bind(broadcast_path).unwrap();
|
let listener = UnixListener::bind(broadcast_path).unwrap();
|
||||||
|
|
||||||
@@ -93,6 +104,7 @@ impl EventManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send any events queued up to the [`stream`].
|
||||||
async fn send_events(&self, stream: UnixStream) -> Result<()> {
|
async fn send_events(&self, stream: UnixStream) -> Result<()> {
|
||||||
let mut writer = stream;
|
let mut writer = stream;
|
||||||
|
|
||||||
@@ -316,7 +328,7 @@ mod tests {
|
|||||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||||
|
|
||||||
// Write a command to the FIFO
|
// Write a command to the FIFO
|
||||||
let cmd = Plugin::SendMessage {
|
let cmd = PluginMsg::SendMessage {
|
||||||
channel: "#test".to_string(),
|
channel: "#test".to_string(),
|
||||||
message: "hello".to_string(),
|
message: "hello".to_string(),
|
||||||
};
|
};
|
||||||
@@ -338,7 +350,7 @@ mod tests {
|
|||||||
.expect("channel closed");
|
.expect("channel closed");
|
||||||
|
|
||||||
match received {
|
match received {
|
||||||
Plugin::SendMessage { channel, message } => {
|
PluginMsg::SendMessage { channel, message } => {
|
||||||
assert_eq!(channel, "#test");
|
assert_eq!(channel, "#test");
|
||||||
assert_eq!(message, "hello");
|
assert_eq!(message, "hello");
|
||||||
}
|
}
|
||||||
@@ -362,15 +374,15 @@ mod tests {
|
|||||||
|
|
||||||
// Write multiple commands
|
// Write multiple commands
|
||||||
let commands = vec![
|
let commands = vec![
|
||||||
Plugin::SendMessage {
|
PluginMsg::SendMessage {
|
||||||
channel: "#chan1".to_string(),
|
channel: "#chan1".to_string(),
|
||||||
message: "first".to_string(),
|
message: "first".to_string(),
|
||||||
},
|
},
|
||||||
Plugin::SendMessage {
|
PluginMsg::SendMessage {
|
||||||
channel: "#chan2".to_string(),
|
channel: "#chan2".to_string(),
|
||||||
message: "second".to_string(),
|
message: "second".to_string(),
|
||||||
},
|
},
|
||||||
Plugin::SendMessage {
|
PluginMsg::SendMessage {
|
||||||
channel: "#chan3".to_string(),
|
channel: "#chan3".to_string(),
|
||||||
message: "third".to_string(),
|
message: "third".to_string(),
|
||||||
},
|
},
|
||||||
@@ -395,7 +407,7 @@ mod tests {
|
|||||||
.expect("channel closed");
|
.expect("channel closed");
|
||||||
|
|
||||||
match first {
|
match first {
|
||||||
Plugin::SendMessage { channel, message } => {
|
PluginMsg::SendMessage { channel, message } => {
|
||||||
assert_eq!(channel, "#chan1");
|
assert_eq!(channel, "#chan1");
|
||||||
assert_eq!(message, "first");
|
assert_eq!(message, "first");
|
||||||
}
|
}
|
||||||
@@ -407,7 +419,7 @@ mod tests {
|
|||||||
.expect("channel closed");
|
.expect("channel closed");
|
||||||
|
|
||||||
match second {
|
match second {
|
||||||
Plugin::SendMessage { channel, message } => {
|
PluginMsg::SendMessage { channel, message } => {
|
||||||
assert_eq!(channel, "#chan2");
|
assert_eq!(channel, "#chan2");
|
||||||
assert_eq!(message, "second");
|
assert_eq!(message, "second");
|
||||||
}
|
}
|
||||||
@@ -419,7 +431,7 @@ mod tests {
|
|||||||
.expect("channel closed");
|
.expect("channel closed");
|
||||||
|
|
||||||
match third {
|
match third {
|
||||||
Plugin::SendMessage { channel, message } => {
|
PluginMsg::SendMessage { channel, message } => {
|
||||||
assert_eq!(channel, "#chan3");
|
assert_eq!(channel, "#chan3");
|
||||||
assert_eq!(message, "third");
|
assert_eq!(message, "third");
|
||||||
}
|
}
|
||||||
@@ -449,7 +461,7 @@ mod tests {
|
|||||||
let tx = pipe::OpenOptions::new().open_sender(&path).unwrap();
|
let tx = pipe::OpenOptions::new().open_sender(&path).unwrap();
|
||||||
let mut tx = tokio::io::BufWriter::new(tx);
|
let mut tx = tokio::io::BufWriter::new(tx);
|
||||||
|
|
||||||
let cmd = Plugin::SendMessage {
|
let cmd = PluginMsg::SendMessage {
|
||||||
channel: "#first".to_string(),
|
channel: "#first".to_string(),
|
||||||
message: "batch1".to_string(),
|
message: "batch1".to_string(),
|
||||||
};
|
};
|
||||||
@@ -465,7 +477,7 @@ mod tests {
|
|||||||
.expect("channel closed");
|
.expect("channel closed");
|
||||||
|
|
||||||
match first {
|
match first {
|
||||||
Plugin::SendMessage { channel, message } => {
|
PluginMsg::SendMessage { channel, message } => {
|
||||||
assert_eq!(channel, "#first");
|
assert_eq!(channel, "#first");
|
||||||
assert_eq!(message, "batch1");
|
assert_eq!(message, "batch1");
|
||||||
}
|
}
|
||||||
@@ -482,7 +494,7 @@ mod tests {
|
|||||||
let tx = pipe::OpenOptions::new().open_sender(&fifo_path).unwrap();
|
let tx = pipe::OpenOptions::new().open_sender(&fifo_path).unwrap();
|
||||||
let mut tx = tokio::io::BufWriter::new(tx);
|
let mut tx = tokio::io::BufWriter::new(tx);
|
||||||
|
|
||||||
let cmd = Plugin::SendMessage {
|
let cmd = PluginMsg::SendMessage {
|
||||||
channel: "#second".to_string(),
|
channel: "#second".to_string(),
|
||||||
message: "batch2".to_string(),
|
message: "batch2".to_string(),
|
||||||
};
|
};
|
||||||
@@ -497,7 +509,7 @@ mod tests {
|
|||||||
.expect("channel closed");
|
.expect("channel closed");
|
||||||
|
|
||||||
match second {
|
match second {
|
||||||
Plugin::SendMessage { channel, message } => {
|
PluginMsg::SendMessage { channel, message } => {
|
||||||
assert_eq!(channel, "#second");
|
assert_eq!(channel, "#second");
|
||||||
assert_eq!(message, "batch2");
|
assert_eq!(message, "batch2");
|
||||||
}
|
}
|
||||||
@@ -524,7 +536,7 @@ mod tests {
|
|||||||
let tx = pipe::OpenOptions::new().open_sender(&fifo_path).unwrap();
|
let tx = pipe::OpenOptions::new().open_sender(&fifo_path).unwrap();
|
||||||
let mut tx = tokio::io::BufWriter::new(tx);
|
let mut tx = tokio::io::BufWriter::new(tx);
|
||||||
|
|
||||||
let cmd1 = Plugin::SendMessage {
|
let cmd1 = PluginMsg::SendMessage {
|
||||||
channel: "#test".to_string(),
|
channel: "#test".to_string(),
|
||||||
message: "first".to_string(),
|
message: "first".to_string(),
|
||||||
};
|
};
|
||||||
@@ -537,7 +549,7 @@ mod tests {
|
|||||||
// Write whitespace line
|
// Write whitespace line
|
||||||
tx.write_all(b" \n").await.unwrap();
|
tx.write_all(b" \n").await.unwrap();
|
||||||
|
|
||||||
let cmd2 = Plugin::SendMessage {
|
let cmd2 = PluginMsg::SendMessage {
|
||||||
channel: "#test".to_string(),
|
channel: "#test".to_string(),
|
||||||
message: "second".to_string(),
|
message: "second".to_string(),
|
||||||
};
|
};
|
||||||
@@ -553,7 +565,7 @@ mod tests {
|
|||||||
.expect("channel closed");
|
.expect("channel closed");
|
||||||
|
|
||||||
match first {
|
match first {
|
||||||
Plugin::SendMessage { channel, message } => {
|
PluginMsg::SendMessage { channel, message } => {
|
||||||
assert_eq!(channel, "#test");
|
assert_eq!(channel, "#test");
|
||||||
assert_eq!(message, "first");
|
assert_eq!(message, "first");
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/lib.rs
14
src/lib.rs
@@ -1,4 +1,5 @@
|
|||||||
// Robotnik libraries
|
#![warn(missing_docs)]
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
use std::{os::unix::fs, sync::Arc};
|
use std::{os::unix::fs, sync::Arc};
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ pub mod plugin;
|
|||||||
pub mod qna;
|
pub mod qna;
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
|
|
||||||
|
pub use chat::Chat;
|
||||||
pub use event::Event;
|
pub use event::Event;
|
||||||
pub use event_manager::EventManager;
|
pub use event_manager::EventManager;
|
||||||
pub use qna::LLMHandle;
|
pub use qna::LLMHandle;
|
||||||
@@ -25,7 +27,9 @@ const DEFAULT_INSTRUCT: &str =
|
|||||||
be sent in a single IRC response according to the specification. Keep answers to
|
be sent in a single IRC response according to the specification. Keep answers to
|
||||||
500 characters or less.";
|
500 characters or less.";
|
||||||
|
|
||||||
// NB: Everything should fail if logging doesn't start properly.
|
/// Initialize all logging facilities.
|
||||||
|
///
|
||||||
|
/// This should cause a panic if there's a failure.
|
||||||
async fn init_logging() {
|
async fn init_logging() {
|
||||||
better_panic::install();
|
better_panic::install();
|
||||||
setup_panic!();
|
setup_panic!();
|
||||||
@@ -37,6 +41,10 @@ async fn init_logging() {
|
|||||||
tracing::subscriber::set_global_default(subscriber).unwrap();
|
tracing::subscriber::set_global_default(subscriber).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets up and runs the main event loop.
|
||||||
|
///
|
||||||
|
/// Should return an error if it's recoverable, but could panic if something
|
||||||
|
/// is particularly bad.
|
||||||
pub async fn run() -> Result<()> {
|
pub async fn run() -> Result<()> {
|
||||||
init_logging().await;
|
init_logging().await;
|
||||||
info!("Starting up.");
|
info!("Starting up.");
|
||||||
@@ -69,7 +77,7 @@ pub async fn run() -> Result<()> {
|
|||||||
let ev_manager = Arc::new(EventManager::new()?);
|
let ev_manager = Arc::new(EventManager::new()?);
|
||||||
let ev_manager_clone = Arc::clone(&ev_manager);
|
let ev_manager_clone = Arc::clone(&ev_manager);
|
||||||
|
|
||||||
let mut c = chat::new(&config, &handle, Arc::clone(&ev_manager)).await?;
|
let mut c = Chat::new(&config, &handle, Arc::clone(&ev_manager)).await?;
|
||||||
|
|
||||||
let (from_plugins, to_chat) = mpsc::channel(100);
|
let (from_plugins, to_chat) = mpsc::channel(100);
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,32 @@
|
|||||||
|
//! Plugin command definitions.
|
||||||
|
|
||||||
|
// Dear future me: If you forget the JSON translations in the future you'll
|
||||||
|
// thank me for the comment overkill.
|
||||||
|
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Message types accepted from plugins.
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub enum Plugin {
|
pub enum PluginMsg {
|
||||||
SendMessage { channel: String, message: String },
|
/// Plugin message indicating the bot should send a [`message`] to [`channel`].
|
||||||
|
/// {
|
||||||
|
/// "SendMessage": {
|
||||||
|
/// "channel": "channel_name",
|
||||||
|
/// "message": "your message here"
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// }
|
||||||
|
SendMessage {
|
||||||
|
/// The IRC channel to send the [`message`] to.
|
||||||
|
channel: String,
|
||||||
|
/// The [`message`] to send.
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Plugin {
|
impl Display for PluginMsg {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::SendMessage { channel, message } => {
|
Self::SendMessage { channel, message } => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Handles communication with a genai compatible LLM.
|
||||||
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use genai::{
|
use genai::{
|
||||||
@@ -8,8 +10,11 @@ use genai::{
|
|||||||
};
|
};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
// NB: Docs are quick and dirty as this might move into a plugin.
|
||||||
|
|
||||||
// Represents an LLM completion source.
|
// Represents an LLM completion source.
|
||||||
// FIXME: Clone is probably temporary.
|
// FIXME: Clone is probably temporary.
|
||||||
|
/// Struct containing information about the LLM.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct LLMHandle {
|
pub struct LLMHandle {
|
||||||
chat_request: ChatRequest,
|
chat_request: ChatRequest,
|
||||||
@@ -18,6 +23,7 @@ pub struct LLMHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LLMHandle {
|
impl LLMHandle {
|
||||||
|
/// Create a new handle.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
api_key: String,
|
api_key: String,
|
||||||
_base_url: impl AsRef<str>,
|
_base_url: impl AsRef<str>,
|
||||||
@@ -44,6 +50,7 @@ impl LLMHandle {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a chat message to the LLM with the response being returned as a [`String`].
|
||||||
pub async fn send_request(&mut self, message: impl Into<String>) -> Result<String> {
|
pub async fn send_request(&mut self, message: impl Into<String>) -> Result<String> {
|
||||||
let mut req = self.chat_request.clone();
|
let mut req = self.chat_request.clone();
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
|
|||||||
12
src/setup.rs
12
src/setup.rs
@@ -1,3 +1,7 @@
|
|||||||
|
//! Handles configuration for the bot.
|
||||||
|
//!
|
||||||
|
//! Both command line, and configuration file options are handled here.
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use color_eyre::{Result, eyre::WrapErr};
|
use color_eyre::{Result, eyre::WrapErr};
|
||||||
use config::Config;
|
use config::Config;
|
||||||
@@ -6,6 +10,7 @@ use std::path::PathBuf;
|
|||||||
use tracing::{info, instrument};
|
use tracing::{info, instrument};
|
||||||
|
|
||||||
// TODO: use [clap(long, short, help_heading = Some(section))]
|
// TODO: use [clap(long, short, help_heading = Some(section))]
|
||||||
|
/// Struct of potential arguments.
|
||||||
#[derive(Clone, Debug, Parser)]
|
#[derive(Clone, Debug, Parser)]
|
||||||
#[command(about, version)]
|
#[command(about, version)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
@@ -30,6 +35,7 @@ pub struct Args {
|
|||||||
pub instruct: Option<String>,
|
pub instruct: Option<String>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
/// Name of the model to use. E.g. 'deepseek-chat'
|
||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -65,11 +71,17 @@ pub struct Args {
|
|||||||
pub use_tls: Option<bool>,
|
pub use_tls: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle for interacting with the bot configuration.
|
||||||
pub struct Setup {
|
pub struct Setup {
|
||||||
|
/// Handle for the configuration file options.
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
|
/// Initialize a new [`Setup`] instance.
|
||||||
|
///
|
||||||
|
/// This reads the settings file which becomes the bot's default configuration.
|
||||||
|
/// These settings shall be overridden by any command line options.
|
||||||
pub async fn init() -> Result<Setup> {
|
pub async fn init() -> Result<Setup> {
|
||||||
// Get arguments. These overrule configuration file, and environment
|
// Get arguments. These overrule configuration file, and environment
|
||||||
// variables if applicable.
|
// variables if applicable.
|
||||||
|
|||||||
@@ -41,12 +41,12 @@ echo "Weather for $1: Sunny, 72°F"
|
|||||||
);
|
);
|
||||||
|
|
||||||
let cmd_dir = CommandDir::new(temp.path());
|
let cmd_dir = CommandDir::new(temp.path());
|
||||||
let message = "!weather 73135";
|
let message = "!weather 10096";
|
||||||
|
|
||||||
// Parse the message
|
// Parse the message
|
||||||
let (command_name, arg) = parse_bot_message(message).unwrap();
|
let (command_name, arg) = parse_bot_message(message).unwrap();
|
||||||
assert_eq!(command_name, "weather");
|
assert_eq!(command_name, "weather");
|
||||||
assert_eq!(arg, "73135");
|
assert_eq!(arg, "10096");
|
||||||
|
|
||||||
// Find and run the command
|
// Find and run the command
|
||||||
let result = cmd_dir.run_command(command_name, arg).await;
|
let result = cmd_dir.run_command(command_name, arg).await;
|
||||||
@@ -54,7 +54,7 @@ echo "Weather for $1: Sunny, 72°F"
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
let bytes = result.unwrap();
|
let bytes = result.unwrap();
|
||||||
let output = String::from_utf8_lossy(&bytes);
|
let output = String::from_utf8_lossy(&bytes);
|
||||||
assert!(output.contains("Weather for 73135"));
|
assert!(output.contains("Weather for 10096"));
|
||||||
assert!(output.contains("Sunny"));
|
assert!(output.contains("Sunny"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ echo "Why did the robot go on vacation? To recharge!"
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_non_bot_message_ignored() {
|
async fn test_non_bot_message_ignored() {
|
||||||
// Messages not starting with ! should be ignored
|
// Messages not starting with ! should be ignored
|
||||||
let messages = ["hello world", "weather 73135", "?help", "/command", ""];
|
let messages = ["hello world", "weather 10096", "?help", "/command", ""];
|
||||||
|
|
||||||
for message in messages {
|
for message in messages {
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
Reference in New Issue
Block a user