From d25129d829e0ebd70b4e60e399fe91c0d80aa1ad Mon Sep 17 00:00:00 2001 From: seth Date: Sun, 16 Jun 2024 07:15:13 -0400 Subject: use libgit2 to track PRs (#10) * nix: don't depend on registry for nixpkgs input * use libgit2 to track PRs * nix: don't use ci devShell as defaul * crates: bump serenity from `9ad74d4` to `0.12.2 * nix: fix cross compiled builds * crates: split more from client * bot-jobs: update remote refs more efficiently * git-tracker: account for HEAD commits * bot-config: use nixpkgs branches from environment * bot-commands: don't display branches prs haven't landed in * git-tracker: return false when commits aren't found this is annoying as a hard error since it turns out github will report garbage merge commit SHAs for PRs that *haven't* been merged yet. yay * bot: improve docs in some places * bot-client: display invite link on start * bot-http: add TeawieClientExt * bot-commands: add /about * docs: update readme todos * nix: enable StateDirectory in module * crates: bump to 0.2.0 --- src/client.rs | 58 --------------------- src/command/mod.rs | 51 ------------------- src/command/ping.rs | 23 --------- src/command/track.rs | 139 --------------------------------------------------- src/handler/mod.rs | 67 ------------------------- src/http/github.rs | 69 ------------------------- src/http/mod.rs | 44 ---------------- src/main.rs | 29 ----------- 8 files changed, 480 deletions(-) delete mode 100644 src/client.rs delete mode 100644 src/command/mod.rs delete mode 100644 src/command/ping.rs delete mode 100644 src/command/track.rs delete mode 100644 src/handler/mod.rs delete mode 100644 src/http/github.rs delete mode 100644 src/http/mod.rs delete mode 100644 src/main.rs (limited to 'src') diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index a779a3b..0000000 --- a/src/client.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::sync::Arc; - -use crate::{ - handler::Handler, - http::{self, HttpClientExt}, -}; - -use eyre::Result; -use serenity::prelude::{Client, GatewayIntents, TypeMapKey}; -use tracing::trace; - -/// Container for [http::Client] -pub struct SharedClient; - -impl TypeMapKey for SharedClient { - type Value = Arc; -} - -/// Fetch our bot token -fn token() -> Result { - let token = std::env::var("DISCORD_BOT_TOKEN")?; - Ok(token) -} - -/// Create our client -#[tracing::instrument] -pub async fn get() -> Client { - let token = token().expect("Couldn't find token in environment! Is DISCORD_BOT_TOKEN set?"); - - let intents = GatewayIntents::default(); - trace!("Creating client"); - let client = Client::builder(token, intents) - .event_handler(Handler) - .await - .expect("Couldn't create a client!"); - - // add state stuff - { - let mut data = client.data.write().await; - trace!("Creating HTTP client"); - let http_client = ::default(); - trace!("Inserting HTTP client into Discord client"); - data.insert::(Arc::new(http_client)) - } - - let shard_manager = client.shard_manager.clone(); - - // gracefully shutdown on ctrl+c - tokio::spawn(async move { - #[cfg(target_family = "unix")] - tokio::signal::ctrl_c() - .await - .expect("Couldn't registrl ctrl+c handler!"); - shard_manager.shutdown_all().await; - }); - - client -} diff --git a/src/command/mod.rs b/src/command/mod.rs deleted file mode 100644 index eda4167..0000000 --- a/src/command/mod.rs +++ /dev/null @@ -1,51 +0,0 @@ -use eyre::{OptionExt, Result}; -use serenity::builder::{ - CreateCommand, CreateInteractionResponse, CreateInteractionResponseMessage, -}; -use serenity::model::application::CommandInteraction; -use serenity::prelude::Context; -use tracing::instrument; - -use crate::client::SharedClient; - -mod ping; -mod track; - -macro_rules! cmd { - ($module: ident) => { - $module::register() - }; -} - -/// Return a list of all our [CreateCommand]s -pub fn to_vec() -> Vec { - vec![cmd!(ping), cmd!(track)] -} - -/// Dispatch our commands from a [CommandInteraction] -#[instrument(skip(ctx))] -pub async fn dispatch(ctx: &Context, command: &CommandInteraction) -> Result<()> { - let command_name = command.data.name.as_str(); - - // grab our http client from the aether - let http = { - let read = ctx.data.read().await; - read.get::() - .ok_or_eyre("Couldn't get shared HTTP client! WHY??????")? - .clone() - }; - - match command_name { - "ping" => ping::respond(ctx, command).await?, - "track" => track::respond(ctx, &http, command).await?, - _ => { - let message = CreateInteractionResponseMessage::new().content(format!( - "It doesn't look like you can use `{command_name}`. Sorry :(" - )); - let response = CreateInteractionResponse::Message(message); - command.create_response(&ctx, response).await? - } - }; - - Ok(()) -} diff --git a/src/command/ping.rs b/src/command/ping.rs deleted file mode 100644 index 1b1b812..0000000 --- a/src/command/ping.rs +++ /dev/null @@ -1,23 +0,0 @@ -use eyre::Result; -use serenity::builder::{ - CreateCommand, CreateInteractionResponse, CreateInteractionResponseMessage, -}; -use serenity::model::application::{CommandInteraction, InstallationContext}; -use serenity::prelude::Context; -use tracing::{instrument, trace}; - -#[instrument] -pub async fn respond(ctx: &Context, command: &CommandInteraction) -> Result<()> { - trace!("Responding to ping command"); - 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/src/command/track.rs b/src/command/track.rs deleted file mode 100644 index 45715f4..0000000 --- a/src/command/track.rs +++ /dev/null @@ -1,139 +0,0 @@ -use crate::http::{Client, GithubClientExt, GITHUB_URL}; - -use eyre::Result; -use futures::future::try_join_all; -use serenity::all::CreateEmbed; -use serenity::builder::{CreateCommand, CreateCommandOption, CreateInteractionResponseFollowup}; -use serenity::model::application::{ - CommandInteraction, CommandOptionType, InstallationContext, ResolvedOption, ResolvedValue, -}; -use serenity::prelude::Context; -use tracing::{instrument, trace}; - -/// All of our tracked branches in nixpkgs -const BRANCHES: [&str; 8] = [ - "master", - "staging", - "nixos-unstable", - "nixos-unstable-small", - "nixos-24.05-small", - "release-24.05", - "nixos-23.11-small", - "release-23.11", -]; - -#[derive(Clone, Debug, Default)] -struct BranchStatus { - repo_owner: String, - repo_name: String, - name: String, -} - -impl BranchStatus { - fn new(repo_owner: String, repo_name: String, name: String) -> Self { - Self { - repo_owner, - repo_name, - name, - } - } - - /// Make a nice friendly string displaying if this branch has a PR merged into it - fn to_status_string(&self, has_pr: bool) -> String { - let emoji = if has_pr { "✅" } else { "❌" }; - format!("`{}` {emoji}", &self.name) - } - - /// Check if this branch has the specified pull request merged into it - #[instrument(skip(http))] - async fn has_pr(&self, http: &Client, pr: u64) -> Result { - let commit = http - .merge_commit_for( - &self.repo_owner, - &self.repo_name, - u64::try_from(pr).unwrap(), - ) - .await?; - - let has_pr = http - .is_commit_in_branch(&self.repo_owner, &self.repo_name, &self.name, &commit) - .await?; - - Ok(has_pr) - } -} - -/// async wrapper for [BranchStatus::to_status_string()] -#[instrument(skip(http))] -async fn collect_status( - http: &Client, - repo_owner: String, - repo_name: String, - branch: String, - pr: u64, -) -> Result { - let status = BranchStatus::new(repo_owner, repo_name, branch); - let has_pr = status.has_pr(http, pr).await?; - let res = status.to_status_string(has_pr); - - Ok(res) -} - -#[instrument(skip_all)] -pub async fn respond(ctx: &Context, http: &Client, command: &CommandInteraction) -> Result<()> { - trace!("Responding to track command"); - - // this will probably take a while - command.defer(&ctx).await?; - - // TODO: make these configurable for nixpkgs forks...or other github repos ig - const REPO_OWNER: &str = "NixOS"; - const REPO_NAME: &str = "nixpkgs"; - - let options = command.data.options(); - - let response = if let Some(ResolvedOption { - value: ResolvedValue::Integer(pr), - .. - }) = options.first() - { - if *pr < 0 { - CreateInteractionResponseFollowup::new().content("PR numbers aren't negative...") - } else { - // TODO: this is gross - let statuses = try_join_all(BRANCHES.iter().map(|&branch| { - collect_status( - http, - REPO_OWNER.to_string(), - REPO_NAME.to_string(), - branch.to_string(), - u64::try_from(*pr).unwrap(), - ) - })) - .await?; - - let embed = CreateEmbed::new() - .title(format!("Nixpkgs PR #{} Status", *pr)) - .url(format!("{GITHUB_URL}/{REPO_OWNER}/{REPO_NAME}/pull/{}", pr)) - .description(statuses.join("\n")); - - CreateInteractionResponseFollowup::new().embed(embed) - } - } else { - CreateInteractionResponseFollowup::new().content("Please provide a valid commit!") - }; - - command.create_followup(&ctx, response).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), - ) -} diff --git a/src/handler/mod.rs b/src/handler/mod.rs deleted file mode 100644 index 47e2774..0000000 --- a/src/handler/mod.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::command; - -use std::error::Error; - -use serenity::async_trait; -use serenity::builder::{CreateEmbed, CreateInteractionResponse, CreateInteractionResponseMessage}; -use serenity::model::{ - application::{Command, Interaction}, - colour::Colour, - gateway::Ready, -}; -use serenity::prelude::{Context, EventHandler}; -use tracing::{debug, error, info, instrument}; - -#[derive(Clone, Copy, Debug)] -pub struct Handler; - -impl Handler { - async fn register_commands(&self, ctx: &Context) -> Result<(), Box> { - let commands = command::to_vec(); - let commands_len = commands.len(); - for command in commands { - Command::create_global_command(&ctx.http, command).await?; - } - - debug!("Registered {} commands", commands_len); - Ok(()) - } -} - -#[async_trait] -impl EventHandler for Handler { - #[instrument(skip_all)] - async fn interaction_create(&self, ctx: Context, interaction: Interaction) { - if let Interaction::Command(command) = interaction { - let command_name = &command.data.name; - debug!("Received command: {}", command_name); - - if let Err(why) = command::dispatch(&ctx, &command).await { - error!( - "Ran into an error while dispatching command {}:\n{why:?}", - command_name - ); - - let embed = CreateEmbed::new() - .title("An error occurred") - .description("Sorry about that!") - .color(Colour::RED); - let message = CreateInteractionResponseMessage::new().embed(embed); - let response = CreateInteractionResponse::Message(message); - - if let Err(why) = command.create_response(&ctx.http, response).await { - error!("Ran into an error while trying to recover from an error!\n{why:?}"); - } - } - } - } - - #[instrument(skip_all)] - async fn ready(&self, ctx: Context, ready: Ready) { - info!("Connected as {}!", ready.user.name); - - if let Err(why) = self.register_commands(&ctx).await { - error!("Couldn't register commands!\n{why:?}"); - }; - } -} diff --git a/src/http/github.rs b/src/http/github.rs deleted file mode 100644 index 8d4f18a..0000000 --- a/src/http/github.rs +++ /dev/null @@ -1,69 +0,0 @@ -use super::{Error, HttpClientExt}; - -use serde::Deserialize; - -pub const GITHUB_URL: &str = "https://github.com"; -pub const GITHUB_API: &str = "https://api.github.com"; - -/// Bad version of `/repos/{owner}/{repo}/{compare}/{ref}...{ref}` -#[derive(Deserialize)] -struct Compare { - status: String, - ahead_by: i32, -} - -/// Bad version of `/repos/{owner}/{repo}/pulls/{pull_number}` -#[derive(Deserialize)] -struct PullRequest { - merge_commit_sha: String, -} - -pub trait GithubClientExt { - /// Get the commit that merged [`pr`] in [`repo_owner`]/[`repo_name`] - async fn merge_commit_for( - &self, - repo_owner: &str, - repo_name: &str, - pr: u64, - ) -> Result; - - /// Check if [`commit`] is in [`branch`] of [`repo_owner`]/[`repo_name`] - async fn is_commit_in_branch( - &self, - repo_owner: &str, - repo_name: &str, - branch_name: &str, - commit: &str, - ) -> Result; -} - -impl GithubClientExt for super::Client { - async fn merge_commit_for( - &self, - repo_owner: &str, - repo_name: &str, - pr: u64, - ) -> Result { - let url = format!("{GITHUB_API}/repos/{repo_owner}/{repo_name}/pulls/{pr}"); - let resp: PullRequest = self.get_json(&url).await?; - let merge_commit = resp.merge_commit_sha; - - Ok(merge_commit) - } - - async fn is_commit_in_branch( - &self, - repo_owner: &str, - repo_name: &str, - branch: &str, - commit: &str, - ) -> Result { - let url = format!( - "https://api.github.com/repos/{repo_owner}/{repo_name}/compare/{branch}...{commit}" - ); - let resp: Compare = self.get_json(&url).await?; - let in_branch = resp.status != "diverged" && resp.ahead_by >= 0; - - Ok(in_branch) - } -} diff --git a/src/http/mod.rs b/src/http/mod.rs deleted file mode 100644 index fa60d67..0000000 --- a/src/http/mod.rs +++ /dev/null @@ -1,44 +0,0 @@ -use serde::de::DeserializeOwned; -use tracing::trace; - -mod github; - -pub use github::*; - -pub type Client = reqwest::Client; -pub type Response = reqwest::Response; -pub type Error = reqwest::Error; - -/// Fun trait for functions we use with [Client] -pub trait HttpClientExt { - fn default() -> Self; - async fn get_request(&self, url: &str) -> Result; - async fn get_json(&self, url: &str) -> Result; -} - -impl HttpClientExt for Client { - fn default() -> Self { - reqwest::Client::builder() - .user_agent(format!( - "nixpkgs-tracker-bot/{}", - option_env!("CARGO_PKG_VERSION").unwrap_or_else(|| "development") - )) - .build() - .unwrap() - } - - async fn get_request(&self, url: &str) -> Result { - trace!("Making GET request to {url}"); - - let resp = self.get(url).send().await?; - resp.error_for_status_ref()?; - - Ok(resp) - } - - async fn get_json(&self, url: &str) -> Result { - let resp = self.get_request(url).await?; - let json = resp.json().await?; - Ok(json) - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 83b2e58..0000000 --- a/src/main.rs +++ /dev/null @@ -1,29 +0,0 @@ -use eyre::Result; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - -mod client; -mod command; -mod handler; -mod http; - -fn init_logging() { - let fmt_layer = tracing_subscriber::fmt::layer().pretty(); - let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "nixpkgs_tracker_bot=info,warn".into()); - - tracing_subscriber::registry() - .with(fmt_layer) - .with(env_filter) - .init(); -} - -#[tokio::main] -async fn main() -> Result<()> { - dotenvy::dotenv().ok(); - init_logging(); - - let mut client = client::get().await; - client.start().await?; - - Ok(()) -} -- cgit v1.2.3