summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/bot-client/Cargo.toml32
-rw-r--r--crates/bot-commands/Cargo.toml34
-rw-r--r--crates/bot-commands/src/track.rs121
-rw-r--r--crates/bot-config/Cargo.toml23
-rw-r--r--crates/bot-consts/Cargo.toml23
-rw-r--r--crates/bot-error/Cargo.toml23
-rw-r--r--crates/bot-error/src/lib.rs1
-rw-r--r--crates/bot-http/Cargo.toml26
-rw-r--r--crates/bot-http/src/github.rs35
-rw-r--r--crates/bot-http/src/lib.rs63
-rw-r--r--crates/bot-http/src/teawie.rs24
-rw-r--r--crates/bot-jobs/Cargo.toml29
-rw-r--r--crates/bot-jobs/src/lib.rs30
-rw-r--r--crates/bot-jobs/src/repo.rs77
-rw-r--r--crates/bot/Cargo.toml26
-rw-r--r--crates/discord-bot/Cargo.toml31
-rw-r--r--crates/discord-bot/src/commands/about.rs (renamed from crates/bot-commands/src/about.rs)22
-rw-r--r--crates/discord-bot/src/commands/mod.rs (renamed from crates/bot-commands/src/lib.rs)0
-rw-r--r--crates/discord-bot/src/commands/ping.rs (renamed from crates/bot-commands/src/ping.rs)5
-rw-r--r--crates/discord-bot/src/commands/track.rs133
-rw-r--r--crates/discord-bot/src/config.rs (renamed from crates/bot-config/src/lib.rs)4
-rw-r--r--crates/discord-bot/src/consts.rs (renamed from crates/bot-consts/src/lib.rs)0
-rw-r--r--crates/discord-bot/src/handler.rs (renamed from crates/bot-client/src/handler.rs)20
-rw-r--r--crates/discord-bot/src/jobs.rs36
-rw-r--r--crates/discord-bot/src/lib.rs (renamed from crates/bot-client/src/lib.rs)19
-rw-r--r--crates/discord-bot/src/main.rs (renamed from crates/bot/src/main.rs)4
-rw-r--r--crates/git-tracker/Cargo.toml28
-rw-r--r--crates/git-tracker/src/lib.rs33
-rw-r--r--crates/git-tracker/src/managed_repository.rs95
-rw-r--r--crates/git-tracker/src/tracker.rs12
-rw-r--r--crates/nixpkgs-tracker-http/Cargo.toml22
-rw-r--r--crates/nixpkgs-tracker-http/src/github.rs40
-rw-r--r--crates/nixpkgs-tracker-http/src/lib.rs28
-rw-r--r--crates/nixpkgs-tracker-http/src/model.rs (renamed from crates/bot-http/src/model.rs)6
-rw-r--r--crates/nixpkgs-tracker-http/src/teawie.rs30
35 files changed, 510 insertions, 625 deletions
diff --git a/crates/bot-client/Cargo.toml b/crates/bot-client/Cargo.toml
deleted file mode 100644
index a2ba2a0..0000000
--- a/crates/bot-client/Cargo.toml
+++ /dev/null
@@ -1,32 +0,0 @@
-[package]
-name = "bot-client"
-version = "0.2.0"
-edition = "2021"
-
-authors = ["seth <getchoo at tuta dot io>"]
-description = "Discord client for nixpkgs-tracker-bot"
-repository = "https://github.com/getchoo/nixpkgs-tracker-bot"
-
-publish = false
-
-[dependencies]
-bot-commands = { workspace = true }
-bot-config = { workspace = true }
-bot-consts = { workspace = true }
-bot-error = { workspace = true }
-bot-http = { workspace = true }
-bot-jobs = { workspace = true }
-log = { workspace = true }
-serenity = { workspace = true }
-tokio = { workspace = true }
-
-[lints.rust]
-unsafe_code = "forbid"
-
-[lints.clippy]
-complexity = "warn"
-correctness = "deny"
-pedantic = "warn"
-perf = "warn"
-style = "warn"
-suspicious = "deny"
diff --git a/crates/bot-commands/Cargo.toml b/crates/bot-commands/Cargo.toml
deleted file mode 100644
index 3594c70..0000000
--- a/crates/bot-commands/Cargo.toml
+++ /dev/null
@@ -1,34 +0,0 @@
-[package]
-name = "bot-commands"
-version = "0.2.0"
-edition = "2021"
-
-authors = ["seth <getchoo at tuta dot io>"]
-description = "Discord application commands for nixpkgs-tracker-bot"
-repository = "https://github.com/getchoo/nixpkgs-tracker-bot"
-
-publish = false
-
-[dependencies]
-bot-config = { workspace = true }
-bot-consts = { workspace = true }
-bot-error = { workspace = true }
-bot-http = { workspace = true }
-git-tracker = { workspace = true }
-log = { workspace = true }
-serenity = { workspace = true }
-
-[lints.rust]
-unsafe_code = "forbid"
-
-[lints.clippy]
-complexity = "warn"
-correctness = "deny"
-pedantic = "warn"
-perf = "warn"
-style = "warn"
-suspicious = "deny"
-# NOTE: THIS ISN'T IN OTHER CRATES BUT IS HERE
-# this is because we don't really care about error docs here
-# and it could mess with poise's comment system in the future :p
-missing-errors-doc = "allow"
diff --git a/crates/bot-commands/src/track.rs b/crates/bot-commands/src/track.rs
deleted file mode 100644
index 1f22d0e..0000000
--- a/crates/bot-commands/src/track.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-use bot_config::Config;
-use bot_consts::{NIXPKGS_REMOTE, NIXPKGS_URL};
-use bot_error::Error;
-use bot_http::{self as http, GithubClientExt};
-use git_tracker::Tracker;
-
-use log::trace;
-use serenity::all::CreateEmbed;
-use serenity::builder::{CreateCommand, CreateCommandOption, CreateInteractionResponseFollowup};
-use serenity::model::application::{
- CommandInteraction, CommandOptionType, InstallationContext, ResolvedOption, ResolvedValue,
-};
-use serenity::prelude::Context;
-
-const REPO_OWNER: &str = "NixOS";
-const REPO_NAME: &str = "nixpkgs";
-
-/// Collect the status of the commit SHA [`commit_sha`] in each of the nixpkgs
-/// branches in [`branches`], using the repository at path [`repository_path`]
-///
-/// # Errors
-///
-/// Will return [`Err`] if we can't start tracking a repository at the given path,
-/// or if we can't determine if the branch has given commit
-fn collect_statuses_in<'a>(
- repository_path: &str,
- commit_sha: &str,
- branches: impl IntoIterator<Item = &'a String>,
-) -> Result<Vec<String>, Error> {
- // start tracking nixpkgs
- let tracker = Tracker::from_path(repository_path)?;
-
- // check to see what branches it's in
- let mut status_results = vec![];
- for branch_name in branches {
- trace!("Checking for commit in {branch_name}");
- let full_branch_name = format!("{NIXPKGS_REMOTE}/{branch_name}");
- let has_pr = tracker.branch_contains_sha(&full_branch_name, commit_sha)?;
-
- if has_pr {
- status_results.push(format!("`{branch_name}` ✅"));
- }
- }
-
- Ok(status_results)
-}
-
-pub async fn respond(
- ctx: &Context,
- http: &http::Client,
- config: &Config,
- command: &CommandInteraction,
-) -> Result<(), Error> {
- // 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("Please provide a valid pull request!");
- command.create_followup(&ctx, resp).await?;
-
- return Ok(());
- };
-
- let Ok(pr_id) = u64::try_from(*pr) else {
- let resp =
- CreateInteractionResponseFollowup::new().content("PR numbers aren't negative...");
- command.create_followup(&ctx, resp).await?;
-
- return Ok(());
- };
-
- // find out what commit our PR was merged in
- let Some(commit_sha) = http.merge_commit_for(REPO_OWNER, REPO_NAME, pr_id).await? 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 = collect_statuses_in(
- &config.nixpkgs_path,
- &commit_sha,
- config.nixpkgs_branches.iter(),
- )?;
-
- // if we don't find the commit in any branches from above, we can pretty safely assume
- // it's an unmerged PR
- let embed_description: String = if status_results.is_empty() {
- "It doesn't look like this PR has been merged yet! (or maybe I just haven't updated)"
- .to_string()
- } else {
- status_results.join("\n")
- };
-
- let embed = CreateEmbed::new()
- .title(format!("Nixpkgs PR #{} Status", *pr))
- .url(format!("{NIXPKGS_URL}/pull/{pr}"))
- .description(embed_description);
-
- 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),
- )
-}
diff --git a/crates/bot-config/Cargo.toml b/crates/bot-config/Cargo.toml
deleted file mode 100644
index 57b9a67..0000000
--- a/crates/bot-config/Cargo.toml
+++ /dev/null
@@ -1,23 +0,0 @@
-[package]
-name = "bot-config"
-version = "0.2.0"
-edition = "2021"
-
-authors = ["seth <getchoo at tuta dot io>"]
-description = "Configuration for nixpkgs-tracker-bot"
-repository = "https://github.com/getchoo/nixpkgs-tracker-bot"
-
-publish = false
-
-[dependencies]
-
-[lints.rust]
-unsafe_code = "forbid"
-
-[lints.clippy]
-complexity = "warn"
-correctness = "deny"
-pedantic = "warn"
-perf = "warn"
-style = "warn"
-suspicious = "deny"
diff --git a/crates/bot-consts/Cargo.toml b/crates/bot-consts/Cargo.toml
deleted file mode 100644
index 16d7726..0000000
--- a/crates/bot-consts/Cargo.toml
+++ /dev/null
@@ -1,23 +0,0 @@
-[package]
-name = "bot-consts"
-version = "0.2.0"
-edition = "2021"
-
-authors = ["seth <getchoo at tuta dot io>"]
-description = "Constants for nixpkgs-tracker-bot"
-repository = "https://github.com/getchoo/nixpkgs-tracker-bot"
-
-publish = false
-
-[dependencies]
-
-[lints.rust]
-unsafe_code = "forbid"
-
-[lints.clippy]
-complexity = "warn"
-correctness = "deny"
-pedantic = "warn"
-perf = "warn"
-style = "warn"
-suspicious = "deny"
diff --git a/crates/bot-error/Cargo.toml b/crates/bot-error/Cargo.toml
deleted file mode 100644
index c6f6ed1..0000000
--- a/crates/bot-error/Cargo.toml
+++ /dev/null
@@ -1,23 +0,0 @@
-[package]
-name = "bot-error"
-version = "0.2.0"
-edition = "2021"
-
-authors = ["seth <getchoo at tuta dot io>"]
-description = "Shared Err variant used for (most of) nixpkgs-tracker-bot"
-repository = "https://github.com/getchoo/nixpkgs-tracker-bot"
-
-publish = false
-
-[dependencies]
-
-[lints.rust]
-unsafe_code = "forbid"
-
-[lints.clippy]
-complexity = "warn"
-correctness = "deny"
-pedantic = "warn"
-perf = "warn"
-style = "warn"
-suspicious = "deny"
diff --git a/crates/bot-error/src/lib.rs b/crates/bot-error/src/lib.rs
deleted file mode 100644
index f34e60e..0000000
--- a/crates/bot-error/src/lib.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub type Error = Box<dyn std::error::Error + Send + Sync>;
diff --git a/crates/bot-http/Cargo.toml b/crates/bot-http/Cargo.toml
deleted file mode 100644
index 0888fda..0000000
--- a/crates/bot-http/Cargo.toml
+++ /dev/null
@@ -1,26 +0,0 @@
-[package]
-name = "bot-http"
-version = "0.2.0"
-edition = "2021"
-
-authors = ["seth <getchoo at tuta dot io>"]
-description = "HTTP client for nixpkgs-tracker-bot"
-repository = "https://github.com/getchoo/nixpkgs-tracker-bot"
-
-publish = false
-
-[dependencies]
-log = { workspace = true }
-reqwest = { version = "0.12.5", default-features = false, features = ["charset", "http2", "rustls-tls", "json"] }
-serde = { version = "1.0.207", features = ["derive"] }
-
-[lints.rust]
-unsafe_code = "forbid"
-
-[lints.clippy]
-complexity = "warn"
-correctness = "deny"
-pedantic = "warn"
-perf = "warn"
-style = "warn"
-suspicious = "deny"
diff --git a/crates/bot-http/src/github.rs b/crates/bot-http/src/github.rs
deleted file mode 100644
index 7822eb8..0000000
--- a/crates/bot-http/src/github.rs
+++ /dev/null
@@ -1,35 +0,0 @@
-use super::{ClientExt as _, Error};
-use crate::model::PullRequest;
-
-use std::future::Future;
-
-const GITHUB_API: &str = "https://api.github.com";
-
-pub trait ClientExt {
- /// Get the commit that merged [`pr`] in [`repo_owner`]/[`repo_name`]
- ///
- /// # Errors
- ///
- /// Will return [`Err`] if the merge commit cannot be found
- fn merge_commit_for(
- &self,
- repo_owner: &str,
- repo_name: &str,
- pr: u64,
- ) -> impl Future<Output = Result<Option<String>, Error>> + Send;
-}
-
-impl ClientExt for super::Client {
- async fn merge_commit_for(
- &self,
- repo_owner: &str,
- repo_name: &str,
- pr: u64,
- ) -> Result<Option<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)
- }
-}
diff --git a/crates/bot-http/src/lib.rs b/crates/bot-http/src/lib.rs
deleted file mode 100644
index ab32cd4..0000000
--- a/crates/bot-http/src/lib.rs
+++ /dev/null
@@ -1,63 +0,0 @@
-use std::future::Future;
-
-use log::trace;
-use serde::de::DeserializeOwned;
-
-mod github;
-mod model;
-mod teawie;
-
-pub use github::ClientExt as GithubClientExt;
-pub use teawie::ClientExt as TeawieClientExt;
-
-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 ClientExt {
- fn default() -> Self;
- fn get_request(&self, url: &str) -> impl Future<Output = Result<Response, Error>> + Send;
- fn get_json<T: DeserializeOwned>(
- &self,
- url: &str,
- ) -> impl Future<Output = Result<T, Error>> + Send;
-}
-
-impl ClientExt for Client {
- /// Create the default [`Client`]
- fn default() -> Self {
- reqwest::Client::builder()
- .user_agent(format!(
- "nixpkgs-tracker-bot/{}",
- option_env!("CARGO_PKG_VERSION").unwrap_or_else(|| "development")
- ))
- .build()
- .unwrap()
- }
-
- /// Perform a GET request to [`url`]
- ///
- /// # Errors
- ///
- /// Will return [`Err`] if the request fails
- 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)
- }
-
- /// Perform a GET request to [`url`] and decode the json response
- ///
- /// # Errors
- ///
- /// Will return [`Err`] if the request fails or cannot be deserialized
- 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/crates/bot-http/src/teawie.rs b/crates/bot-http/src/teawie.rs
deleted file mode 100644
index ea4f53e..0000000
--- a/crates/bot-http/src/teawie.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-use super::{ClientExt as _, Error};
-use crate::model::RandomTeawie;
-
-use std::future::Future;
-
-const TEAWIE_API: &str = "https://api.getchoo.com";
-
-pub trait ClientExt {
- /// Get a random teawie
- ///
- /// # Errors
- ///
- /// Will return [`Err`] if the request fails or the response cannot be deserialized
- fn random_teawie(&self) -> impl Future<Output = Result<Option<String>, Error>> + Send;
-}
-
-impl ClientExt for super::Client {
- async fn random_teawie(&self) -> Result<Option<String>, Error> {
- let url = format!("{TEAWIE_API}/random_teawie");
- let resp: RandomTeawie = self.get_json(&url).await?;
-
- Ok(resp.url)
- }
-}
diff --git a/crates/bot-jobs/Cargo.toml b/crates/bot-jobs/Cargo.toml
deleted file mode 100644
index 21b0248..0000000
--- a/crates/bot-jobs/Cargo.toml
+++ /dev/null
@@ -1,29 +0,0 @@
-[package]
-name = "bot-jobs"
-version = "0.2.0"
-edition = "2021"
-
-authors = ["seth <getchoo at tuta dot io>"]
-description = "Background jobs for nixpkgs-tracker-bot"
-repository = "https://github.com/getchoo/nixpkgs-tracker-bot"
-
-publish = false
-
-[dependencies]
-bot-config = { workspace = true }
-bot-consts = { workspace = true }
-bot-error = { workspace = true }
-git2 = { workspace = true, features = ["https"] }
-log = { workspace = true }
-tokio = { workspace = true }
-
-[lints.rust]
-unsafe_code = "forbid"
-
-[lints.clippy]
-complexity = "warn"
-correctness = "deny"
-pedantic = "warn"
-perf = "warn"
-style = "warn"
-suspicious = "deny"
diff --git a/crates/bot-jobs/src/lib.rs b/crates/bot-jobs/src/lib.rs
deleted file mode 100644
index d65c929..0000000
--- a/crates/bot-jobs/src/lib.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-use bot_config::Config;
-use bot_error::Error;
-
-use std::time::Duration;
-
-use log::error;
-
-mod repo;
-
-/// Run our jobs an initial time, then loop them on a separate thread
-///
-/// # Errors
-///
-/// Will return [`Err`] if any jobs fail
-pub fn dispatch(config: Config) -> Result<(), Error> {
- repo::fetch_or_update_repository(&config.nixpkgs_path, &config.nixpkgs_branches)?;
-
- tokio::spawn(async move {
- loop {
- tokio::time::sleep(Duration::from_secs(repo::TTL_SECS)).await;
- if let Err(why) =
- repo::fetch_or_update_repository(&config.nixpkgs_path, &config.nixpkgs_branches)
- {
- error!("Failed to fetch or update repository!\n{why:?}");
- };
- }
- });
-
- Ok(())
-}
diff --git a/crates/bot-jobs/src/repo.rs b/crates/bot-jobs/src/repo.rs
deleted file mode 100644
index 4d3e214..0000000
--- a/crates/bot-jobs/src/repo.rs
+++ /dev/null
@@ -1,77 +0,0 @@
-use bot_consts::{NIXPKGS_REMOTE, NIXPKGS_URL};
-use bot_error::Error;
-
-use std::{io::Write, path::Path};
-
-use git2::{AutotagOption, FetchOptions, RemoteCallbacks, Repository};
-use log::{debug, info, trace, warn};
-
-pub const TTL_SECS: u64 = 60 * 5; // 5 minutes
-
-// much of this is shamelessly lifted from
-// https://github.com/rust-lang/git2-rs/blob/9a5c9706ff578c936be644dd1e8fe155bdc4d129/examples/pull.rs
-
-/// basic set of options for fetching from remotes
-fn fetch_options<'a>() -> FetchOptions<'a> {
- let mut remote_callbacks = RemoteCallbacks::new();
- remote_callbacks.transfer_progress(|progress| {
- if progress.received_objects() == progress.total_objects() {
- trace!(
- "Resolving deltas {}/{}\r",
- progress.indexed_deltas(),
- progress.total_deltas()
- );
- } else {
- trace!(
- "Received {}/{} objects ({}) in {} bytes\r",
- progress.received_objects(),
- progress.total_objects(),
- progress.indexed_objects(),
- progress.received_bytes()
- );
- }
- std::io::stdout().flush().ok();
- true
- });
-
- let mut fetch_opts = FetchOptions::new();
- fetch_opts.remote_callbacks(remote_callbacks);
-
- fetch_opts
-}
-
-/// update the given branches in the [`repository`] using the nixpkgs remote
-fn update_branches_in(repository: &Repository, branches: &[String]) -> Result<(), Error> {
- let mut remote = repository.find_remote(NIXPKGS_REMOTE)?;
- // download all the refs
- remote.download(branches, Some(&mut fetch_options()))?;
- remote.disconnect()?;
- // and (hopefully) update what they refer to for later
- remote.update_tips(None, true, AutotagOption::Auto, None)?;
-
- Ok(())
-}
-
-pub fn fetch_or_update_repository(path: &str, branches: &[String]) -> Result<(), Error> {
- // Open our repository or clone it if it doesn't exist
- let path = Path::new(path);
- let repository = if path.exists() {
- Repository::open(path)?
- } else {
- warn!(
- "Couldn't find repository at {}! Cloning a fresh one from {NIXPKGS_URL}",
- path.display()
- );
- Repository::clone(NIXPKGS_URL, path)?;
- info!("Finished cloning to {}", path.display());
-
- // bail early as we already have a fresh copy
- return Ok(());
- };
-
- debug!("Updating repository at {}", path.display());
- update_branches_in(&repository, branches)?;
- debug!("Finished updating!");
-
- Ok(())
-}
diff --git a/crates/bot/Cargo.toml b/crates/bot/Cargo.toml
deleted file mode 100644
index 69feace..0000000
--- a/crates/bot/Cargo.toml
+++ /dev/null
@@ -1,26 +0,0 @@
-[package]
-name = "nixpkgs-tracker-bot"
-version = "0.2.0"
-edition = "2021"
-
-authors = ["seth <getchoo at tuta dot io>"]
-description = "A small Discord app that helps you track where nixpkgs PRs have reached"
-repository = "https://github.com/getchoo/nixpkgs-tracker-bot"
-
-[dependencies]
-bot-error = { workspace = true }
-bot-client = { workspace = true }
-dotenvy = "0.15.7"
-env_logger = "0.11.5"
-tokio = { workspace = true }
-
-[lints.rust]
-unsafe_code = "forbid"
-
-[lints.clippy]
-complexity = "warn"
-correctness = "deny"
-pedantic = "warn"
-perf = "warn"
-style = "warn"
-suspicious = "deny"
diff --git a/crates/discord-bot/Cargo.toml b/crates/discord-bot/Cargo.toml
new file mode 100644
index 0000000..dd35306
--- /dev/null
+++ b/crates/discord-bot/Cargo.toml
@@ -0,0 +1,31 @@
+[package]
+name = "discord-bot"
+version.workspace = true
+authors.workspace = true
+edition.workspace = true
+description = "Small Discord app that helps you track where nixpkgs PRs have reached"
+repository.workspace = true
+license.workspace = true
+
+publish = false
+
+[[bin]]
+name = "nixpkgs-tracker-bot"
+path = "src/main.rs"
+
+[dependencies]
+dotenvy = "0.15.7"
+env_logger = "0.11.5"
+eyre = "0.6.12"
+git-tracker.workspace = true
+log.workspace = true
+nixpkgs-tracker-http.workspace = true
+serenity = { version = "0.12.2", features = ["unstable_discord_api"] }
+tokio = { version = "1.39.2", features = [
+ "macros",
+ "rt-multi-thread",
+ "signal"
+] }
+
+[lints]
+workspace = true
diff --git a/crates/bot-commands/src/about.rs b/crates/discord-bot/src/commands/about.rs
index 2e5efae..e663faf 100644
--- a/crates/bot-commands/src/about.rs
+++ b/crates/discord-bot/src/commands/about.rs
@@ -1,6 +1,9 @@
-use bot_error::Error;
-use bot_http::TeawieClientExt;
+use std::sync::Arc;
+use crate::http::TeawieClientExt;
+
+use eyre::Result;
+use log::warn;
use serenity::builder::{
CreateCommand, CreateEmbed, CreateEmbedFooter, CreateInteractionResponse,
CreateInteractionResponseMessage,
@@ -11,11 +14,10 @@ use serenity::prelude::Context;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
-pub async fn respond(
- ctx: &Context,
- http: &bot_http::Client,
- command: &CommandInteraction,
-) -> Result<(), Error> {
+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.")
@@ -25,9 +27,13 @@ pub async fn respond(
("Issues/Feature Requests", &format!("[getchoo/nixpkgs-tracker-bot/issues]({REPOSITORY}/issues)"), true)
]);
- if let Some(teawie_url) = http.random_teawie().await? {
+ 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);
diff --git a/crates/bot-commands/src/lib.rs b/crates/discord-bot/src/commands/mod.rs
index 79fce17..79fce17 100644
--- a/crates/bot-commands/src/lib.rs
+++ b/crates/discord-bot/src/commands/mod.rs
diff --git a/crates/bot-commands/src/ping.rs b/crates/discord-bot/src/commands/ping.rs
index b18a0b6..30150dc 100644
--- a/crates/bot-commands/src/ping.rs
+++ b/crates/discord-bot/src/commands/ping.rs
@@ -1,12 +1,11 @@
-use bot_error::Error;
-
+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<(), Error> {
+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?;
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),
+ )
+}
diff --git a/crates/bot-config/src/lib.rs b/crates/discord-bot/src/config.rs
index 0691884..5076eb9 100644
--- a/crates/bot-config/src/lib.rs
+++ b/crates/discord-bot/src/config.rs
@@ -1,3 +1,5 @@
+use crate::consts::NIXPKGS_REMOTE;
+
use std::env;
/// The Discord client's configuration
@@ -14,7 +16,7 @@ impl Config {
fn split_string_list(branches: &str) -> Vec<String> {
branches
.split(',')
- .map(|branch| branch.trim().to_string())
+ .map(|branch| format!("{NIXPKGS_REMOTE}/{}", branch.trim()))
.collect()
}
diff --git a/crates/bot-consts/src/lib.rs b/crates/discord-bot/src/consts.rs
index 9396da0..9396da0 100644
--- a/crates/bot-consts/src/lib.rs
+++ b/crates/discord-bot/src/consts.rs
diff --git a/crates/bot-client/src/handler.rs b/crates/discord-bot/src/handler.rs
index 2cd0082..4abb99b 100644
--- a/crates/bot-client/src/handler.rs
+++ b/crates/discord-bot/src/handler.rs
@@ -1,6 +1,6 @@
-use crate::{SharedConfig, SharedHttp};
-use bot_error::Error;
+use crate::{commands, SharedConfig, SharedHttp};
+use eyre::{OptionExt, Result};
use log::{debug, error, info, trace, warn};
use serenity::all::CreateBotAuthParameters;
use serenity::async_trait;
@@ -19,8 +19,8 @@ use serenity::prelude::{Context, EventHandler};
pub struct Handler;
impl Handler {
- async fn register_commands(&self, ctx: &Context) -> Result<(), Error> {
- let commands = bot_commands::to_vec();
+ async fn register_commands(&self, ctx: &Context) -> Result<()> {
+ let commands = commands::to_vec();
let commands_len = commands.len();
for command in commands {
Command::create_global_command(&ctx.http, command).await?;
@@ -31,7 +31,7 @@ impl Handler {
}
/// Dispatch our commands from a [`CommandInteraction`]
- async fn dispatch_command(ctx: &Context, command: &CommandInteraction) -> Result<(), Error> {
+ async fn dispatch_command(ctx: &Context, command: &CommandInteraction) -> Result<()> {
let command_name = command.data.name.as_str();
// grab our configuration & http client from the aether
@@ -39,19 +39,19 @@ impl Handler {
let read = ctx.data.read().await;
let http = read
.get::<SharedHttp>()
- .ok_or("Couldn't get shared HTTP client! WHY??????")?
+ .ok_or_eyre("Couldn't get shared HTTP client! WHY??????")?
.clone();
let config = read
.get::<SharedConfig>()
- .ok_or("Couldn't get shared bot configuration!")?
+ .ok_or_eyre("Couldn't get shared bot configuration!")?
.clone();
(http, config)
};
match command_name {
- "about" => bot_commands::about::respond(ctx, &http, command).await?,
- "ping" => bot_commands::ping::respond(ctx, command).await?,
- "track" => bot_commands::track::respond(ctx, &http, &config, command).await?,
+ "about" => commands::about::respond(ctx, &http, command).await?,
+ "ping" => commands::ping::respond(ctx, command).await?,
+ "track" => commands::track::respond(ctx, &http, &config, command).await?,
_ => {
let message = CreateInteractionResponseMessage::new().content(format!(
"It doesn't look like you can use `{command_name}`. Sorry :("
diff --git a/crates/discord-bot/src/jobs.rs b/crates/discord-bot/src/jobs.rs
new file mode 100644
index 0000000..40d34cc
--- /dev/null
+++ b/crates/discord-bot/src/jobs.rs
@@ -0,0 +1,36 @@
+use crate::{config::Config, consts::NIXPKGS_REMOTE, consts::NIXPKGS_URL};
+
+use std::{path::Path, time::Duration};
+
+use eyre::Result;
+use git_tracker::ManagedRepository;
+use log::error;
+
+const TTL_SECS: u64 = 60 * 5; // 5 minutes
+
+/// Run our jobs an initial time, then loop them on a separate thread
+///
+/// # Errors
+///
+/// Will return [`Err`] if any jobs fail
+pub fn dispatch(config: Config) -> Result<()> {
+ let managed_repository = ManagedRepository {
+ path: Path::new(&config.nixpkgs_path).to_path_buf(),
+ tracked_branches: config.nixpkgs_branches,
+ upstream_remote_url: NIXPKGS_URL.to_string(),
+ upstream_remote_name: NIXPKGS_REMOTE.to_string(),
+ };
+
+ managed_repository.fetch_or_update()?;
+
+ tokio::spawn(async move {
+ loop {
+ tokio::time::sleep(Duration::from_secs(TTL_SECS)).await;
+ if let Err(why) = managed_repository.fetch_or_update() {
+ error!("Could not fetch or update repository!\n{why:?}");
+ };
+ }
+ });
+
+ Ok(())
+}
diff --git a/crates/bot-client/src/lib.rs b/crates/discord-bot/src/lib.rs
index 851b853..7ac2e98 100644
--- a/crates/bot-client/src/lib.rs
+++ b/crates/discord-bot/src/lib.rs
@@ -1,15 +1,18 @@
-use bot_config::Config;
-use bot_error::Error;
-use bot_http as http;
-
use std::sync::Arc;
+use eyre::Result;
use log::trace;
use serenity::prelude::{Client, GatewayIntents, TypeMapKey};
+mod commands;
+mod config;
+mod consts;
mod handler;
+mod jobs;
+use config::Config;
use handler::Handler;
+use nixpkgs_tracker_http as http;
/// Container for [`http::Client`]
struct SharedHttp;
@@ -26,7 +29,7 @@ impl TypeMapKey for SharedConfig {
}
/// Fetch our bot token
-fn token() -> Result<String, Error> {
+fn token() -> Result<String> {
let token = std::env::var("DISCORD_BOT_TOKEN")?;
Ok(token)
}
@@ -41,7 +44,7 @@ fn token() -> Result<String, Error> {
/// # Panics
///
/// Will [`panic!`] if the bot token isn't found or the ctrl+c handler can't be made
-pub async fn get() -> Result<Client, Error> {
+pub async fn client() -> Result<Client> {
let token = token().expect("Couldn't find token in environment! Is DISCORD_BOT_TOKEN set?");
let intents = GatewayIntents::default();
@@ -51,7 +54,7 @@ pub async fn get() -> Result<Client, Error> {
.await?;
// add state stuff
- let http_client = <http::Client as http::ClientExt>::default();
+ let http_client = <http::Client as http::Ext>::default();
let config = Config::from_env()?;
{
@@ -73,7 +76,7 @@ pub async fn get() -> Result<Client, Error> {
});
// run our jobs
- bot_jobs::dispatch(config)?;
+ jobs::dispatch(config)?;
Ok(client)
}
diff --git a/crates/bot/src/main.rs b/crates/discord-bot/src/main.rs
index 390e79b..acbf9fd 100644
--- a/crates/bot/src/main.rs
+++ b/crates/discord-bot/src/main.rs
@@ -1,9 +1,9 @@
#[tokio::main]
-async fn main() -> Result<(), bot_error::Error> {
+async fn main() -> eyre::Result<()> {
dotenvy::dotenv().ok();
env_logger::try_init()?;
- let mut client = bot_client::get().await?;
+ let mut client = discord_bot::client().await?;
client.start().await?;
Ok(())
diff --git a/crates/git-tracker/Cargo.toml b/crates/git-tracker/Cargo.toml
index 09584cb..4805502 100644
--- a/crates/git-tracker/Cargo.toml
+++ b/crates/git-tracker/Cargo.toml
@@ -1,27 +1,17 @@
[package]
name = "git-tracker"
-version = "0.2.0"
-edition = "2021"
-
-authors = ["seth <getchoo at tuta dot io>"]
-description = "A library that helps you track commits and branches in a Git repository"
-repository = "https://github.com/getchoo/nixpkgs-tracker-bot"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+description = "Library that helps you track commits and branches in a Git repository"
+repository.workspace = true
publish = false
[dependencies]
-git2 = { workspace = true }
-log = { workspace = true }
+git2 = { version = "0.19.0", default-features = false, features = ["https"] }
+log.workspace = true
thiserror = "1.0.63"
-[lints.rust]
-async_fn_in_trait = "allow"
-unsafe_code = "forbid"
-
-[lints.clippy]
-complexity = "warn"
-correctness = "deny"
-pedantic = "warn"
-perf = "warn"
-style = "warn"
-suspicious = "deny"
+[lints]
+workspace = true
diff --git a/crates/git-tracker/src/lib.rs b/crates/git-tracker/src/lib.rs
index cb0907b..0bf17dc 100644
--- a/crates/git-tracker/src/lib.rs
+++ b/crates/git-tracker/src/lib.rs
@@ -1,4 +1,35 @@
//! A library that helps you track commits and branches in a Git repository
+use log::trace;
+mod managed_repository;
mod tracker;
-pub use tracker::{Error, Tracker};
+pub use managed_repository::ManagedRepository;
+pub use tracker::Tracker;
+
+/// Collect the status of the commit SHA [`commit_sha`] in each of the nixpkgs
+/// branches in [`branches`], using the repository at path [`repository_path`]
+///
+/// NOTE: `branches` should contain the full ref (i.e., `origin/main`)
+///
+/// # Errors
+///
+/// Will return [`Err`] if we can't start tracking a repository at the given path,
+/// or if we can't determine if the branch has given commit
+pub fn collect_statuses_in(
+ repository_path: &str,
+ commit_sha: &str,
+ branches: &Vec<String>,
+) -> Result<Vec<(String, bool)>, tracker::Error> {
+ // start tracking nixpkgs
+ let tracker = Tracker::from_path(repository_path)?;
+
+ // check to see what branches it's in
+ let mut status_results = Vec::new();
+ for branch_name in branches {
+ trace!("Checking for commit in {branch_name}");
+ let has_pr = tracker.branch_contains_sha(branch_name, commit_sha)?;
+ status_results.push((branch_name.to_string(), has_pr));
+ }
+
+ Ok(status_results)
+}
diff --git a/crates/git-tracker/src/managed_repository.rs b/crates/git-tracker/src/managed_repository.rs
new file mode 100644
index 0000000..0a41bd0
--- /dev/null
+++ b/crates/git-tracker/src/managed_repository.rs
@@ -0,0 +1,95 @@
+use git2::{AutotagOption, FetchOptions, RemoteCallbacks, RemoteUpdateFlags, Repository};
+use log::{debug, info, trace, warn};
+use std::{io::Write, path::PathBuf};
+
+// much of this is shamelessly lifted from
+// https://github.com/rust-lang/git2-rs/blob/9a5c9706ff578c936be644dd1e8fe155bdc4d129/examples/pull.rs
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ #[error("libgit2 error")]
+ Git(#[from] git2::Error),
+}
+
+pub struct ManagedRepository {
+ pub path: PathBuf,
+ pub tracked_branches: Vec<String>,
+ pub upstream_remote_url: String,
+ pub upstream_remote_name: String,
+}
+
+impl ManagedRepository {
+ /// basic set of options for fetching from remotes
+ fn fetch_options<'a>() -> FetchOptions<'a> {
+ let mut remote_callbacks = RemoteCallbacks::new();
+ remote_callbacks.transfer_progress(|progress| {
+ if progress.received_objects() == progress.total_objects() {
+ trace!(
+ "Resolving deltas {}/{}\r",
+ progress.indexed_deltas(),
+ progress.total_deltas()
+ );
+ } else {
+ trace!(
+ "Received {}/{} objects ({}) in {} bytes\r",
+ progress.received_objects(),
+ progress.total_objects(),
+ progress.indexed_objects(),
+ progress.received_bytes()
+ );
+ }
+ std::io::stdout().flush().ok();
+ true
+ });
+
+ let mut fetch_opts = FetchOptions::new();
+ fetch_opts.remote_callbacks(remote_callbacks);
+
+ fetch_opts
+ }
+
+ /// Update the given branches in the [`repository`] using the nixpkgs remote
+ fn update_branches_in(&self, repository: &Repository) -> Result<(), Error> {
+ let mut remote = repository.find_remote(&self.upstream_remote_name)?;
+ // download all the refs
+ remote.download(&self.tracked_branches, Some(&mut Self::fetch_options()))?;
+ remote.disconnect()?;
+ // and (hopefully) update what they refer to for later
+ remote.update_tips(
+ None,
+ RemoteUpdateFlags::UPDATE_FETCHHEAD,
+ AutotagOption::Auto,
+ None,
+ )?;
+
+ Ok(())
+ }
+
+ /// Fetch the repository or update it if it exists
+ ///
+ /// # Errors
+ /// Will return [`Err`] if the repository cannot be opened, cloned, or updated
+ pub fn fetch_or_update(&self) -> Result<(), Error> {
+ // Open our repository or clone it if it doesn't exist
+ let repository = if self.path.exists() {
+ Repository::open(self.path.as_path())?
+ } else {
+ warn!(
+ "Couldn't find repository at {}! Cloning a fresh one from {}",
+ self.path.display(),
+ self.upstream_remote_url
+ );
+ Repository::clone(&self.upstream_remote_url, self.path.as_path())?;
+ info!("Finished cloning to {}", self.path.display());
+
+ // bail early as we already have a fresh copy
+ return Ok(());
+ };
+
+ debug!("Updating repository at {}", self.path.display());
+ self.update_branches_in(&repository)?;
+ debug!("Finished updating!");
+
+ Ok(())
+ }
+}
diff --git a/crates/git-tracker/src/tracker.rs b/crates/git-tracker/src/tracker.rs
index e26f82d..e6a3f54 100644
--- a/crates/git-tracker/src/tracker.rs
+++ b/crates/git-tracker/src/tracker.rs
@@ -2,6 +2,11 @@ use std::path::Path;
use git2::{Branch, BranchType, Commit, ErrorCode, Oid, Reference, Repository};
+/// Helper struct for tracking Git objects
+pub struct Tracker {
+ repository: Repository,
+}
+
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("libgit2 error")]
@@ -10,11 +15,6 @@ pub enum Error {
RepositoryPathNotFound(String),
}
-/// Helper struct for tracking Git objects
-pub struct Tracker {
- repository: Repository,
-}
-
impl Tracker {
/// Create a new [`Tracker`] using the repository at [`path`]
///
@@ -76,7 +76,7 @@ impl Tracker {
.repository
.graph_descendant_of(head.id(), commit.id())?;
- Ok(has_commit || is_head)
+ Ok(is_head || has_commit)
}
/// Check if a [`Branch`] named [`branch_name`] has a commit with the SHA [`commit_sha`]
diff --git a/crates/nixpkgs-tracker-http/Cargo.toml b/crates/nixpkgs-tracker-http/Cargo.toml
new file mode 100644
index 0000000..45b897d
--- /dev/null
+++ b/crates/nixpkgs-tracker-http/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "nixpkgs-tracker-http"
+version.workspace = true
+authors.workspace = true
+edition.workspace = true
+repository.workspace = true
+license.workspace = true
+
+publish = false
+
+[dependencies]
+log.workspace = true
+reqwest = { version = "0.12.5", default-features = false, features = [
+ "charset",
+ "http2",
+ "rustls-tls",
+ "json"
+] }
+serde = { version = "1.0.204", features = ["derive"] }
+
+[lints]
+workspace = true
diff --git a/crates/nixpkgs-tracker-http/src/github.rs b/crates/nixpkgs-tracker-http/src/github.rs
new file mode 100644
index 0000000..12ee832
--- /dev/null
+++ b/crates/nixpkgs-tracker-http/src/github.rs
@@ -0,0 +1,40 @@
+use super::{Error, PullRequest};
+
+use std::future::Future;
+
+use log::trace;
+
+const GITHUB_API: &str = "https://api.github.com";
+
+pub trait Ext {
+ /// GET `/repos/{repo_owner}/{repo_name}/pulls/{id}`
+ ///
+ /// # Errors
+ ///
+ /// Will return [`Err`] if the merge commit cannot be found
+ fn pull_request(
+ &self,
+ repo_owner: &str,
+ repo_name: &str,
+ id: u64,
+ ) -> impl Future<Output = Result<PullRequest, Error>> + Send;
+}
+
+impl Ext for super::Client {
+ async fn pull_request(
+ &self,
+ repo_owner: &str,
+ repo_name: &str,
+ id: u64,
+ ) -> Result<PullRequest, Error> {
+ let url = format!("{GITHUB_API}/repos/{repo_owner}/{repo_name}/pulls/{id}");
+
+ let request = self.get(&url).build()?;
+ trace!("Making GET request to `{}`", request.url());
+ let response = self.execute(request).await?;
+ response.error_for_status_ref()?;
+ let pull_request: PullRequest = response.json().await?;
+
+ Ok(pull_request)
+ }
+}
diff --git a/crates/nixpkgs-tracker-http/src/lib.rs b/crates/nixpkgs-tracker-http/src/lib.rs
new file mode 100644
index 0000000..873ebb8
--- /dev/null
+++ b/crates/nixpkgs-tracker-http/src/lib.rs
@@ -0,0 +1,28 @@
+mod github;
+mod model;
+mod teawie;
+
+pub use github::Ext as GitHubClientExt;
+pub use model::*;
+pub use teawie::Ext as TeawieClientExt;
+
+pub type Client = reqwest::Client;
+pub type Error = reqwest::Error;
+
+/// Fun trait for functions we use with [Client]
+pub trait Ext {
+ fn default() -> Self;
+}
+
+impl Ext for Client {
+ /// Create the default [`Client`]
+ fn default() -> Self {
+ reqwest::Client::builder()
+ .user_agent(format!(
+ "nixpkgs-tracker-bot/{}",
+ option_env!("CARGO_PKG_VERSION").unwrap_or_else(|| "development")
+ ))
+ .build()
+ .unwrap()
+ }
+}
diff --git a/crates/bot-http/src/model.rs b/crates/nixpkgs-tracker-http/src/model.rs
index afd4717..9439538 100644
--- a/crates/bot-http/src/model.rs
+++ b/crates/nixpkgs-tracker-http/src/model.rs
@@ -3,6 +3,11 @@ use serde::Deserialize;
/// Bad version of `/repos/{owner}/{repo}/pulls/{pull_number}` for Github's api
#[derive(Clone, Debug, Deserialize)]
pub struct PullRequest {
+ pub html_url: String,
+ pub number: u64,
+ pub title: String,
+ pub merged: bool,
+ pub merged_at: Option<String>,
pub merge_commit_sha: Option<String>,
}
@@ -10,4 +15,5 @@ pub struct PullRequest {
#[derive(Clone, Debug, Deserialize)]
pub struct RandomTeawie {
pub url: Option<String>,
+ pub error: Option<String>,
}
diff --git a/crates/nixpkgs-tracker-http/src/teawie.rs b/crates/nixpkgs-tracker-http/src/teawie.rs
new file mode 100644
index 0000000..97af63c
--- /dev/null
+++ b/crates/nixpkgs-tracker-http/src/teawie.rs
@@ -0,0 +1,30 @@
+use crate::{Error, RandomTeawie};
+
+use std::future::Future;
+
+use log::trace;
+
+const TEAWIE_API: &str = "https://api.getchoo.com";
+
+pub trait Ext {
+ /// Get a random teawie
+ ///
+ /// # Errors
+ ///
+ /// Will return [`Err`] if the request fails or the response cannot be deserialized
+ fn random_teawie(&self) -> impl Future<Output = Result<RandomTeawie, Error>> + Send;
+}
+
+impl Ext for super::Client {
+ async fn random_teawie(&self) -> Result<RandomTeawie, Error> {
+ let url = format!("{TEAWIE_API}/random_teawie");
+
+ let request = self.get(&url).build()?;
+ trace!("Making GET request to {}", request.url());
+ let response = self.execute(request).await?;
+ response.error_for_status_ref()?;
+ let random_teawie: RandomTeawie = response.json().await?;
+
+ Ok(random_teawie)
+ }
+}