mod commands;
mod runner;
use std::env;
use dotenvy::dotenv_override;
use pyo3::PyResult;
use serenity::async_trait;
use serenity::futures::channel::mpsc;
use serenity::futures::{SinkExt, StreamExt};
use serenity::http::CacheHttp;
use serenity::model::application::interaction::{Interaction, InteractionResponseType};
use serenity::model::channel::Message;
use serenity::model::gateway::Ready;
use serenity::model::id::GuildId;
use serenity::prelude::*;
use tokio::spawn;
use tracing::{debug, info, trace, warn};
use tracing_subscriber::EnvFilter;
use crate::runner::{JobUpdate, NewJob};
struct Handler {
guild_id: GuildId,
tx: RwLock<mpsc::Sender<NewJob>>,
}
#[async_trait]
impl EventHandler for Handler {
async fn message(&self, ctx: Context, new_message: Message) {
let channel = new_message.channel_id.to_channel(&ctx.http).await.unwrap();
debug!(
"Received a message on {}: {:#?}",
channel, new_message.content
);
if new_message.author.id == ctx.cache().unwrap().current_user_id() {
trace!("Message is from me, ignoring");
return;
}
if new_message.author.bot {
trace!("Message is from a bot, ignoring");
return;
}
if new_message.content.starts_with("DO: ") {
self.do_task(&ctx, &new_message).await;
return;
}
new_message.channel_id.say(&ctx.http, "oui!").await.unwrap();
}
async fn ready(&self, ctx: Context, ready: Ready) {
info!("{} is connected!", ready.user.name);
let commands = GuildId::set_application_commands(&self.guild_id, &ctx.http, |commands| {
commands.create_application_command(|command| commands::ping::register(command))
})
.await
.unwrap();
info!(
"I now have the following guild slash commands: {:#?}",
commands.iter().map(|c| c.name.clone()).collect::<Vec<_>>()
);
}
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
if let Interaction::ApplicationCommand(command) = interaction {
info!("Received command interaction: {:#?}", command);
let content = match command.data.name.as_str() {
"ping" => commands::ping::run(&command.data.options),
_ => "not implemented :(".to_string(),
};
if let Err(why) = command
.create_interaction_response(&ctx.http, |response| {
response
.kind(InteractionResponseType::ChannelMessageWithSource)
.interaction_response_data(|message| message.content(content))
})
.await
{
info!("Cannot respond to slash command: {}", why);
}
}
}
}
impl Handler {
async fn do_task(&self, ctx: &Context, new_message: &Message) {
let max_steps = 12;
let task = new_message.content[4..].to_string();
if task.is_empty() {
warn!("Empty task, ignoring");
new_message
.channel_id
.say(&ctx.http, "Please provide a task: 'DO: <task>'")
.await
.unwrap();
return;
}
let (tx, mut rx) = mpsc::channel::<JobUpdate>(20);
self.tx
.write()
.await
.send(NewJob::new(task, max_steps, false, tx))
.await
.unwrap();
let thread = new_message
.channel_id
.create_private_thread(&ctx.http, |thread| {
let thread_name = format!("{}'s task", new_message.author.name);
let thread_name = if thread_name.len() > 100 {
thread_name[..100].to_string()
} else {
thread_name
};
thread.name(thread_name).auto_archive_duration(1440)
})
.await
.unwrap();
thread.id.join_thread(&ctx.http).await.unwrap();
thread
.id
.add_thread_member(&ctx.http, new_message.author.id)
.await
.unwrap();
info!("Added {} to thread: {}", new_message.author.name, thread.id);
thread
.send_message(&ctx.http, |message| {
message
.content("Let me warm up my engines...")
.allowed_mentions(|mentions| mentions.replied_user(true))
})
.await
.unwrap();
while let Some(job_update) = rx.next().await {
debug!("Received job update: {:#?}", job_update);
let msgs = match job_update {
JobUpdate::Completed(v) => Some(v),
JobUpdate::Vec(v) => Some(v),
JobUpdate::FailedToStart(e) => Some(e),
JobUpdate::ToolError(e) => Some(e),
JobUpdate::Over => None,
};
if let Some(msgs) = msgs {
for txt in msgs {
thread
.send_message(&ctx.http, |message| {
message
.content(txt)
.allowed_mentions(|mentions| mentions.replied_user(true))
})
.await
.unwrap();
}
}
}
thread
.send_message(&ctx.http, |message| {
message
.content("bye bye")
.allowed_mentions(|mentions| mentions.replied_user(true))
})
.await
.unwrap();
}
}
#[pyo3_asyncio::tokio::main]
async fn main() -> PyResult<()> {
let _ = dotenv_override();
tracing_subscriber::fmt()
.compact()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_default())
.init();
let guild_id = GuildId(
env::var("GUILD_ID")
.expect("Expected GUILD_ID in environment")
.parse()
.expect("GUILD_ID must be an integer"),
);
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
let (tx, rx) = mpsc::channel(100);
let mut runner = runner::Runner::new(rx).await;
for (key, _) in env::vars() {
env::remove_var(key);
}
assert!(env::vars().next().is_none(), "Environment is not empty");
let event_handler = Handler {
guild_id,
tx: RwLock::new(tx),
};
let intents = GatewayIntents::DIRECT_MESSAGES
| GatewayIntents::MESSAGE_CONTENT
| GatewayIntents::GUILD_MESSAGES
| GatewayIntents::GUILD_MESSAGE_REACTIONS;
let mut client = Client::builder(token, intents)
.event_handler(event_handler)
.await
.expect("Error creating client");
spawn(async move {
if let Err(why) = client.start().await {
info!("Client error: {:?}", why);
}
});
runner.run().await;
Ok(())
}