summaryrefslogtreecommitdiff
path: root/crates/discord-bot/src/commands
diff options
context:
space:
mode:
Diffstat (limited to 'crates/discord-bot/src/commands')
-rw-r--r--crates/discord-bot/src/commands/about.rs50
-rw-r--r--crates/discord-bot/src/commands/mod.rs17
-rw-r--r--crates/discord-bot/src/commands/ping.rs20
-rw-r--r--crates/discord-bot/src/commands/track.rs133
4 files changed, 220 insertions, 0 deletions
diff --git a/crates/discord-bot/src/commands/about.rs b/crates/discord-bot/src/commands/about.rs
new file mode 100644
index 0000000..e663faf
--- /dev/null
+++ b/crates/discord-bot/src/commands/about.rs
@@ -0,0 +1,50 @@
+use std::sync::Arc;
+
+use crate::http::TeawieClientExt;
+
+use eyre::Result;
+use log::warn;
+use serenity::builder::{
+ CreateCommand, CreateEmbed, CreateEmbedFooter, CreateInteractionResponse,
+ CreateInteractionResponseMessage,
+};
+use serenity::model::application::{CommandInteraction, InstallationContext};
+use serenity::prelude::Context;
+
+const VERSION: &str = env!("CARGO_PKG_VERSION");
+const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
+
+pub async fn respond<T>(ctx: &Context, http: &Arc<T>, command: &CommandInteraction) -> Result<()>
+where
+ T: TeawieClientExt,
+{
+ let mut embed = CreateEmbed::new()
+ .title("About nixpkgs-tracker-bot")
+ .description("I help track what branches PRs to nixpkgs have reached. If you've used [Nixpkgs Pull Request Tracker](https://nixpk.gs/pr-tracker.html), you probably know what this is about.")
+ .fields([
+ ("Version", VERSION, true),
+ ("Source code", &format!("[getchoo/nixpkgs-tracker-bot]({REPOSITORY})"), true),
+ ("Issues/Feature Requests", &format!("[getchoo/nixpkgs-tracker-bot/issues]({REPOSITORY}/issues)"), true)
+ ]);
+
+ let random_teawie = http.random_teawie().await?;
+
+ if let Some(teawie_url) = random_teawie.url {
+ let footer = CreateEmbedFooter::new("Images courtesy of @sympathytea");
+ embed = embed.image(teawie_url).footer(footer);
+ } else if let Some(error) = random_teawie.error {
+ warn!("Error from TeawieAPI: {error:#?}");
+ };
+
+ let message = CreateInteractionResponseMessage::new().embed(embed);
+ let response = CreateInteractionResponse::Message(message);
+ command.create_response(&ctx, response).await?;
+
+ Ok(())
+}
+
+pub fn register() -> CreateCommand {
+ CreateCommand::new("about")
+ .description("Learn more about me")
+ .add_integration_type(InstallationContext::User)
+}
diff --git a/crates/discord-bot/src/commands/mod.rs b/crates/discord-bot/src/commands/mod.rs
new file mode 100644
index 0000000..79fce17
--- /dev/null
+++ b/crates/discord-bot/src/commands/mod.rs
@@ -0,0 +1,17 @@
+use serenity::builder::CreateCommand;
+
+pub mod about;
+pub mod ping;
+pub mod track;
+
+macro_rules! cmd {
+ ($module: ident) => {
+ $module::register()
+ };
+}
+
+/// Return a list of all our [`CreateCommand`]s
+#[must_use]
+pub fn to_vec() -> Vec<CreateCommand> {
+ vec![cmd!(about), cmd!(ping), cmd!(track)]
+}
diff --git a/crates/discord-bot/src/commands/ping.rs b/crates/discord-bot/src/commands/ping.rs
new file mode 100644
index 0000000..30150dc
--- /dev/null
+++ b/crates/discord-bot/src/commands/ping.rs
@@ -0,0 +1,20 @@
+use eyre::Result;
+use serenity::builder::{
+ CreateCommand, CreateInteractionResponse, CreateInteractionResponseMessage,
+};
+use serenity::model::application::{CommandInteraction, InstallationContext};
+use serenity::prelude::Context;
+
+pub async fn respond(ctx: &Context, command: &CommandInteraction) -> Result<()> {
+ let message = CreateInteractionResponseMessage::new().content("Pong!");
+ let response = CreateInteractionResponse::Message(message);
+ command.create_response(&ctx, response).await?;
+
+ Ok(())
+}
+
+pub fn register() -> CreateCommand {
+ CreateCommand::new("ping")
+ .description("Check if the bot is up")
+ .add_integration_type(InstallationContext::User)
+}
diff --git a/crates/discord-bot/src/commands/track.rs b/crates/discord-bot/src/commands/track.rs
new file mode 100644
index 0000000..f071ebf
--- /dev/null
+++ b/crates/discord-bot/src/commands/track.rs
@@ -0,0 +1,133 @@
+use crate::{config::Config, consts::NIXPKGS_REMOTE, http::GitHubClientExt};
+
+use std::sync::Arc;
+
+use eyre::Result;
+use log::debug;
+use serenity::builder::{
+ CreateCommand, CreateCommandOption, CreateEmbed, CreateInteractionResponseFollowup,
+};
+use serenity::model::{
+ application::{
+ CommandInteraction, CommandOptionType, InstallationContext, ResolvedOption, ResolvedValue,
+ },
+ Timestamp,
+};
+use serenity::prelude::Context;
+
+const REPO_OWNER: &str = "NixOS";
+const REPO_NAME: &str = "nixpkgs";
+
+pub async fn respond<T>(
+ ctx: &Context,
+ http: &Arc<T>,
+ config: &Config,
+ command: &CommandInteraction,
+) -> Result<()>
+where
+ T: GitHubClientExt,
+{
+ // this will probably take a while
+ command.defer(&ctx).await?;
+
+ let options = command.data.options();
+ let Some(ResolvedOption {
+ value: ResolvedValue::Integer(pr),
+ ..
+ }) = options.first()
+ else {
+ let resp = CreateInteractionResponseFollowup::new()
+ .content("PR numbers aren't negative or that big...");
+ command.create_followup(&ctx, resp).await?;
+
+ return Ok(());
+ };
+
+ let Ok(id) = u64::try_from(*pr) else {
+ let resp = CreateInteractionResponseFollowup::new()
+ .content("PR numbers aren't negative or that big...");
+ command.create_followup(&ctx, resp).await?;
+
+ return Ok(());
+ };
+
+ // find out what commit our PR was merged in
+ let pull_request = http.pull_request(REPO_OWNER, REPO_NAME, id).await?;
+ if !pull_request.merged {
+ let response = CreateInteractionResponseFollowup::new()
+ .content("It looks like that PR isn't merged yet! Try again when it is 😄");
+ command.create_followup(&ctx, response).await?;
+
+ return Ok(());
+ }
+
+ // seems older PRs may not have this
+ let Some(commit_sha) = pull_request.merge_commit_sha else {
+ let response = CreateInteractionResponseFollowup::new()
+ .content("It seems this pull request is very old. I can't track it");
+ command.create_followup(&ctx, response).await?;
+
+ return Ok(());
+ };
+
+ let status_results = git_tracker::collect_statuses_in(
+ &config.nixpkgs_path,
+ &commit_sha,
+ &config.nixpkgs_branches,
+ )?;
+
+ // find branches containing our PR and trim the remote ref prefix
+ let found_branches: Vec<String> = status_results
+ .iter()
+ .filter(|&(_, has_pr)| *has_pr)
+ .map(|(branch_name, _)| {
+ // remove the ref prefix that we add in our Config struct
+ let start_pos = format!("{NIXPKGS_REMOTE}/").len();
+ branch_name[start_pos..].to_string()
+ })
+ .collect();
+
+ // if we didn't find any, bail
+ if found_branches.is_empty() {
+ let response = CreateInteractionResponseFollowup::new()
+ .content("This PR has been merged...but I can't seem to find it anywhere. I might not be tracking it's base branch");
+ command.create_followup(&ctx, response).await?;
+
+ return Ok(());
+ }
+
+ let mut embed = CreateEmbed::new()
+ .title(format!("Nixpkgs PR #{} Status", pull_request.number))
+ .url(&pull_request.html_url)
+ .description(&pull_request.title)
+ .fields(
+ found_branches
+ .iter()
+ .map(|branch_name| (branch_name, "✅", true)),
+ );
+
+ if let Some(merged_at) = pull_request.merged_at {
+ if let Ok(timestamp) = Timestamp::parse(&merged_at) {
+ embed = embed.timestamp(timestamp);
+ } else {
+ debug!("Couldn't parse timestamp from GitHub! Ignoring.");
+ }
+ } else {
+ debug!("Couldn't find `merged_at` information for a supposedly merged PR! Ignoring.");
+ }
+
+ let resp = CreateInteractionResponseFollowup::new().embed(embed);
+ command.create_followup(&ctx, resp).await?;
+
+ Ok(())
+}
+
+pub fn register() -> CreateCommand {
+ CreateCommand::new("track")
+ .description("Track a nixpkgs PR")
+ .add_integration_type(InstallationContext::User)
+ .add_option(
+ CreateCommandOption::new(CommandOptionType::Integer, "pull_request", "PR to track")
+ .required(true),
+ )
+}