summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client.rs58
-rw-r--r--src/command/mod.rs51
-rw-r--r--src/command/ping.rs23
-rw-r--r--src/command/track.rs139
-rw-r--r--src/handler/mod.rs67
-rw-r--r--src/http/github.rs69
-rw-r--r--src/http/mod.rs44
-rw-r--r--src/main.rs29
8 files changed, 0 insertions, 480 deletions
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<http::Client>;
-}
-
-/// Fetch our bot token
-fn token() -> Result<String> {
- 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 = <http::Client as HttpClientExt>::default();
- trace!("Inserting HTTP client into Discord client");
- data.insert::<SharedClient>(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<CreateCommand> {
- 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::<SharedClient>()
- .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<bool> {
- 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<String> {
- 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<dyn Error>> {
- 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<String, Error>;
-
- /// 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<bool, Error>;
-}
-
-impl GithubClientExt for super::Client {
- async fn merge_commit_for(
- &self,
- repo_owner: &str,
- repo_name: &str,
- pr: u64,
- ) -> Result<String, Error> {
- 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<bool, Error> {
- 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<Response, Error>;
- async fn get_json<T: DeserializeOwned>(&self, url: &str) -> Result<T, Error>;
-}
-
-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<Response, Error> {
- trace!("Making GET request to {url}");
-
- let resp = self.get(url).send().await?;
- resp.error_for_status_ref()?;
-
- Ok(resp)
- }
-
- async fn get_json<T: DeserializeOwned>(&self, url: &str) -> Result<T, Error> {
- 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(())
-}