summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorseth <[email protected]>2024-06-16 07:15:13 -0400
committerGitHub <[email protected]>2024-06-16 07:15:13 -0400
commitd25129d829e0ebd70b4e60e399fe91c0d80aa1ad (patch)
tree2a62992f2980f9fed2204ef5ef708a0228998cf1
parenta0bfcc1587e3cef1b8f6fa0508a280fc48c82231 (diff)
use libgit2 to track PRs (#10)v0.2.0
* 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
-rw-r--r--.env.template9
-rw-r--r--.github/workflows/ci.yaml2
-rw-r--r--.github/workflows/docker.yaml2
-rw-r--r--.github/workflows/update-flake.yaml11
-rw-r--r--Cargo.lock413
-rw-r--r--Cargo.toml39
-rw-r--r--README.md14
-rw-r--r--crates/bot-client/Cargo.toml32
-rw-r--r--crates/bot-client/src/handler.rs112
-rw-r--r--crates/bot-client/src/lib.rs79
-rw-r--r--crates/bot-commands/Cargo.toml34
-rw-r--r--crates/bot-commands/src/about.rs44
-rw-r--r--crates/bot-commands/src/lib.rs17
-rw-r--r--crates/bot-commands/src/ping.rs (renamed from src/command/ping.rs)8
-rw-r--r--crates/bot-commands/src/track.rs121
-rw-r--r--crates/bot-config/Cargo.toml23
-rw-r--r--crates/bot-config/src/lib.rs36
-rw-r--r--crates/bot-consts/Cargo.toml23
-rw-r--r--crates/bot-consts/src/lib.rs5
-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.rs (renamed from src/http/mod.rs)31
-rw-r--r--crates/bot-http/src/model.rs13
-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/bot/src/main.rs10
-rw-r--r--crates/git-tracker/Cargo.toml27
-rw-r--r--crates/git-tracker/src/lib.rs4
-rw-r--r--crates/git-tracker/src/tracker.rs109
-rw-r--r--flake.lock5
-rw-r--r--flake.nix33
-rw-r--r--nix/module.nix18
-rw-r--r--nix/package.nix12
-rw-r--r--nix/static.nix19
-rw-r--r--src/client.rs58
-rw-r--r--src/command/mod.rs51
-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/main.rs29
45 files changed, 1334 insertions, 655 deletions
diff --git a/.env.template b/.env.template
index cb6a9c1..86fc934 100644
--- a/.env.template
+++ b/.env.template
@@ -1,4 +1,9 @@
+# shellcheck shell=sh
+# shellcheck disable=SC2034
+
DISCORD_BOT_TOKEN=""
+BOT_NIXPKGS_PATH=""
+BOT_NIXPKGS_BRANCHES="staging,staging-next,master,nixpkgs-unstable,nixos-unstable-small,nixos-unstable,nixos-24.05-small,nixos-24.05,nixpkgs-24.05-darwin"
-RUST_LOG="nixpkgs_tracker_bot=debug,warn"
-RUST_BACKTRACE="1"
+RUST_LOG="bot=debug,warn"
+RUST_BACKTRACE=1
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 94518e0..34ced56 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -48,7 +48,7 @@ jobs:
uses: actions/checkout@v4
- name: Install Nix
- uses: cachix/install-nix-action@v27
+ uses: DeterminateSystems/nix-installer-action@v12
- name: Run checks
run: |
diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index b4823fe..bf9e530 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -34,7 +34,7 @@ jobs:
uses: actions/checkout@v4
- name: Install Nix
- uses: cachix/install-nix-action@v27
+ uses: DeterminateSystems/nix-installer-action@v12
- name: Setup Nix cache
uses: DeterminateSystems/magic-nix-cache-action@v7
diff --git a/.github/workflows/update-flake.yaml b/.github/workflows/update-flake.yaml
index ed334bd..e1a7112 100644
--- a/.github/workflows/update-flake.yaml
+++ b/.github/workflows/update-flake.yaml
@@ -9,7 +9,6 @@ on:
jobs:
update:
name: Run update
-
runs-on: ubuntu-latest
permissions:
@@ -21,12 +20,10 @@ jobs:
uses: actions/checkout@v4
- name: Install Nix
- uses: cachix/install-nix-action@v27
+ uses: DeterminateSystems/nix-installer-action@v12
- - name: Update lockfile & make PR
+ - name: Update flake.lock & make PR
uses: DeterminateSystems/update-flake-lock@v22
- id: update
with:
- commit-msg: 'nix: update flake.lock'
- pr-title: 'nix: update flake.lock'
- token: ${{ github.token }}
+ commit-msg: "nix: update flake.lock"
+ pr-title: "nix: update flake.lock"
diff --git a/Cargo.lock b/Cargo.lock
index d5147a9..3892106 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -42,6 +42,55 @@ dependencies = [
]
[[package]]
+name = "anstream"
+version = "0.6.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
name = "arrayvec"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -122,6 +171,67 @@ dependencies = [
]
[[package]]
+name = "bot-client"
+version = "0.2.0"
+dependencies = [
+ "bot-commands",
+ "bot-config",
+ "bot-consts",
+ "bot-error",
+ "bot-http",
+ "bot-jobs",
+ "log",
+ "serenity",
+ "tokio",
+]
+
+[[package]]
+name = "bot-commands"
+version = "0.2.0"
+dependencies = [
+ "bot-config",
+ "bot-consts",
+ "bot-error",
+ "bot-http",
+ "git-tracker",
+ "log",
+ "serenity",
+]
+
+[[package]]
+name = "bot-config"
+version = "0.2.0"
+
+[[package]]
+name = "bot-consts"
+version = "0.2.0"
+
+[[package]]
+name = "bot-error"
+version = "0.2.0"
+
+[[package]]
+name = "bot-http"
+version = "0.2.0"
+dependencies = [
+ "log",
+ "reqwest 0.12.4",
+ "serde",
+]
+
+[[package]]
+name = "bot-jobs"
+version = "0.2.0"
+dependencies = [
+ "bot-config",
+ "bot-consts",
+ "bot-error",
+ "git2",
+ "log",
+ "tokio",
+]
+
+[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -181,6 +291,11 @@ name = "cc"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
+dependencies = [
+ "jobserver",
+ "libc",
+ "once_cell",
+]
[[package]]
name = "cfg-if"
@@ -202,9 +317,16 @@ dependencies = [
]
[[package]]
+name = "colorchoice"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
+
+[[package]]
name = "command_attr"
version = "0.5.2"
-source = "git+https://github.com/serenity-rs/serenity?branch=current#9ad74d41209380591a3458afbc9e21ba1cec2132"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88da8d7e9fe6f30d8e3fcf72d0f84102b49de70fece952633e8439e89bdc7631"
dependencies = [
"proc-macro2",
"quote",
@@ -326,6 +448,29 @@ dependencies = [
]
[[package]]
+name = "env_filter"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "humantime",
+ "log",
+]
+
+[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -351,16 +496,6 @@ dependencies = [
]
[[package]]
-name = "eyre"
-version = "0.6.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
-dependencies = [
- "indenter",
- "once_cell",
-]
-
-[[package]]
name = "fastrand"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -399,7 +534,6 @@ checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
- "futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@@ -423,17 +557,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
-name = "futures-executor"
-version = "0.3.30"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
-dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -517,6 +640,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
+name = "git-tracker"
+version = "0.2.0"
+dependencies = [
+ "git2",
+ "log",
+ "thiserror",
+]
+
+[[package]]
+name = "git2"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70"
+dependencies = [
+ "bitflags 2.5.0",
+ "libc",
+ "libgit2-sys",
+ "log",
+ "openssl-probe",
+ "openssl-sys",
+ "url",
+]
+
+[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -641,6 +788,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
name = "hyper"
version = "0.14.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -769,12 +922,6 @@ dependencies = [
]
[[package]]
-name = "indenter"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
-
-[[package]]
name = "indexmap"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -791,12 +938,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
+[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
+name = "jobserver"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "js-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -806,12 +968,6 @@ dependencies = [
]
[[package]]
-name = "lazy_static"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
-
-[[package]]
name = "levenshtein"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -824,6 +980,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
+name = "libgit2-sys"
+version = "0.16.2+1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
name = "linux-raw-sys"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -846,15 +1027,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
-name = "matchers"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
-dependencies = [
- "regex-automata 0.1.10",
-]
-
-[[package]]
name = "memchr"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -913,27 +1085,13 @@ dependencies = [
[[package]]
name = "nixpkgs-tracker-bot"
-version = "0.1.0"
+version = "0.2.0"
dependencies = [
+ "bot-client",
+ "bot-error",
"dotenvy",
- "eyre",
- "futures",
- "reqwest 0.12.4",
- "serde",
- "serenity",
+ "env_logger",
"tokio",
- "tracing",
- "tracing-subscriber",
-]
-
-[[package]]
-name = "nu-ansi-term"
-version = "0.46.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
-dependencies = [
- "overload",
- "winapi",
]
[[package]]
@@ -977,10 +1135,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
-name = "overload"
-version = "0.1.1"
+name = "openssl-probe"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
[[package]]
name = "parking_lot"
@@ -1044,6 +1214,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
+name = "pkg-config"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+
+[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1131,17 +1307,8 @@ checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
dependencies = [
"aho-corasick",
"memchr",
- "regex-automata 0.4.6",
- "regex-syntax 0.8.3",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
-dependencies = [
- "regex-syntax 0.6.29",
+ "regex-automata",
+ "regex-syntax",
]
[[package]]
@@ -1152,17 +1319,11 @@ checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax 0.8.3",
+ "regex-syntax",
]
[[package]]
name = "regex-syntax"
-version = "0.6.29"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
-
-[[package]]
-name = "regex-syntax"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
@@ -1465,7 +1626,8 @@ dependencies = [
[[package]]
name = "serenity"
version = "0.12.2"
-source = "git+https://github.com/serenity-rs/serenity?branch=current#9ad74d41209380591a3458afbc9e21ba1cec2132"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "880a04106592d0a8f5bdacb1d935889bfbccb4a14f7074984d9cd857235d34ac"
dependencies = [
"arrayvec",
"async-trait",
@@ -1510,15 +1672,6 @@ dependencies = [
]
[[package]]
-name = "sharded-slab"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
-dependencies = [
- "lazy_static",
-]
-
-[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1673,16 +1826,6 @@ dependencies = [
]
[[package]]
-name = "thread_local"
-version = "1.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
-dependencies = [
- "cfg-if",
- "once_cell",
-]
-
-[[package]]
name = "time"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1864,36 +2007,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
- "valuable",
-]
-
-[[package]]
-name = "tracing-log"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
-dependencies = [
- "log",
- "once_cell",
- "tracing-core",
-]
-
-[[package]]
-name = "tracing-subscriber"
-version = "0.3.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
-dependencies = [
- "matchers",
- "nu-ansi-term",
- "once_cell",
- "regex",
- "sharded-slab",
- "smallvec",
- "thread_local",
- "tracing",
- "tracing-core",
- "tracing-log",
]
[[package]]
@@ -2025,16 +2138,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
name = "uwl"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0"
[[package]]
-name = "valuable"
-version = "0.1.0"
+name = "vcpkg"
+version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
@@ -2172,22 +2291,6 @@ dependencies = [
]
[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
name = "winapi-util"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2197,12 +2300,6 @@ dependencies = [
]
[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
-[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index ff39f61..e33af20 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,21 +1,32 @@
-[package]
-name = "nixpkgs-tracker-bot"
-version = "0.1.0"
-edition = "2021"
+[workspace]
+members = [
+ "crates/bot",
+ "crates/bot-client",
+ "crates/bot-config",
+ "crates/bot-consts",
+ "crates/bot-error",
+ "crates/bot-http",
+ "crates/bot-jobs",
+ "crates/git-tracker"
+]
+resolver = "2"
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[workspace.dependencies]
+bot = { path = "./crates/bot" }
+bot-client = { path = "./crates/bot-client" }
+bot-commands = { path = "./crates/bot-commands" }
+bot-config = { path = "./crates/bot-config" }
+bot-consts = { path = "./crates/bot-consts" }
+bot-error = { path = "./crates/bot-error" }
+bot-http = { path = "./crates/bot-http" }
+bot-jobs = { path = "./crates/bot-jobs" }
+git-tracker = { path = "./crates/git-tracker" }
-[dependencies]
-dotenvy = "0.15.7"
-eyre = "0.6.12"
-futures = "0.3.30"
-reqwest = { version = "0.12.4", default-features = false, features = ["charset", "http2", "rustls-tls", "json"] }
-serde = { version = "1.0.203", features = ["derive"] }
-serenity = { git = "https://github.com/serenity-rs/serenity", branch = "current", version = "0.12.1", features = ["unstable_discord_api"] }
+git2 = { version = "0.18.3", default-features = false }
+log = "0.4.21"
+serenity = { version = "0.12.2", features = ["unstable_discord_api"] }
tokio = { version = "1.38.0", features = [
"macros",
"rt-multi-thread",
"signal"
] }
-tracing = "0.1.40"
-tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
diff --git a/README.md b/README.md
index 584d77b..3d85b78 100644
--- a/README.md
+++ b/README.md
@@ -2,20 +2,6 @@
A small Discord ~~bot~~ app(!) that helps you track where [nixpkgs](https://github.com/NixOS/nixpkgs) PRs have reached
-## Currently supported branches
-
-- master
-- staging
-- nixos-unstable
-- nixos-unstable-small
-- nixos-24.05-small
-- release-24.05
-- nixos-23.11-small
-- release-23.11
-
## TODO
-- [ ] Cache responses (to avoid rate limiting)
-- [ ] Allow for authenticated requests to GH (to avoid rate limiting)
-- [ ] Don't make so many API requests for each invocation (to avoid rate limiting...this is a problem see?)
- [ ] Switch to poise after https://github.com/serenity-rs/poise/pull/266
diff --git a/crates/bot-client/Cargo.toml b/crates/bot-client/Cargo.toml
new file mode 100644
index 0000000..a2ba2a0
--- /dev/null
+++ b/crates/bot-client/Cargo.toml
@@ -0,0 +1,32 @@
+[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-client/src/handler.rs b/crates/bot-client/src/handler.rs
new file mode 100644
index 0000000..2cd0082
--- /dev/null
+++ b/crates/bot-client/src/handler.rs
@@ -0,0 +1,112 @@
+use crate::{SharedConfig, SharedHttp};
+use bot_error::Error;
+
+use log::{debug, error, info, trace, warn};
+use serenity::all::CreateBotAuthParameters;
+use serenity::async_trait;
+use serenity::builder::{
+ CreateEmbed, CreateInteractionResponse, CreateInteractionResponseFollowup,
+ CreateInteractionResponseMessage,
+};
+use serenity::model::{
+ application::{Command, CommandInteraction, Interaction},
+ colour::Colour,
+ gateway::Ready,
+};
+use serenity::prelude::{Context, EventHandler};
+
+#[derive(Clone, Copy, Debug)]
+pub struct Handler;
+
+impl Handler {
+ async fn register_commands(&self, ctx: &Context) -> Result<(), Error> {
+ let commands = bot_commands::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(())
+ }
+
+ /// Dispatch our commands from a [`CommandInteraction`]
+ async fn dispatch_command(ctx: &Context, command: &CommandInteraction) -> Result<(), Error> {
+ let command_name = command.data.name.as_str();
+
+ // grab our configuration & http client from the aether
+ let (http, config) = {
+ let read = ctx.data.read().await;
+ let http = read
+ .get::<SharedHttp>()
+ .ok_or("Couldn't get shared HTTP client! WHY??????")?
+ .clone();
+ let config = read
+ .get::<SharedConfig>()
+ .ok_or("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?,
+ _ => {
+ 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(())
+ }
+
+ async fn invite_link(ctx: &Context) {
+ if let Ok(invite_link) = CreateBotAuthParameters::new().auto_client_id(ctx).await {
+ let link = invite_link.build();
+ info!("You can install me as an app at {link}");
+ } else {
+ warn!("Couldn't figure out our own client ID! Something might be wrong");
+ }
+ }
+}
+
+#[async_trait]
+impl EventHandler for Handler {
+ /// Dispatch our commands and try to handle errors from them
+ async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
+ if let Interaction::Command(command) = interaction {
+ let command_name = &command.data.name;
+ trace!("Received command: {}", command_name);
+
+ if let Err(why) = Handler::dispatch_command(&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 response = CreateInteractionResponseFollowup::new().embed(embed);
+
+ if let Err(why) = command.create_followup(&ctx.http, response).await {
+ error!("Ran into an error while trying to recover from an error!\n{why:?}");
+ }
+ }
+ }
+ }
+
+ async fn ready(&self, ctx: Context, ready: Ready) {
+ info!("Connected as {}!", ready.user.name);
+ Handler::invite_link(&ctx).await;
+
+ if let Err(why) = self.register_commands(&ctx).await {
+ error!("Couldn't register commands!\n{why:?}");
+ };
+ }
+}
diff --git a/crates/bot-client/src/lib.rs b/crates/bot-client/src/lib.rs
new file mode 100644
index 0000000..851b853
--- /dev/null
+++ b/crates/bot-client/src/lib.rs
@@ -0,0 +1,79 @@
+use bot_config::Config;
+use bot_error::Error;
+use bot_http as http;
+
+use std::sync::Arc;
+
+use log::trace;
+use serenity::prelude::{Client, GatewayIntents, TypeMapKey};
+
+mod handler;
+
+use handler::Handler;
+
+/// Container for [`http::Client`]
+struct SharedHttp;
+
+impl TypeMapKey for SharedHttp {
+ type Value = Arc<http::Client>;
+}
+
+/// Container for [`Config`]
+struct SharedConfig;
+
+impl TypeMapKey for SharedConfig {
+ type Value = Arc<Config>;
+}
+
+/// Fetch our bot token
+fn token() -> Result<String, Error> {
+ let token = std::env::var("DISCORD_BOT_TOKEN")?;
+ Ok(token)
+}
+
+/// Create our client
+///
+/// # Errors
+///
+/// Will return [`Err`] if a [`Client`] cannot be created or configuration
+/// cannot be created from the environment.
+///
+/// # 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> {
+ 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?;
+
+ // add state stuff
+ let http_client = <http::Client as http::ClientExt>::default();
+ let config = Config::from_env()?;
+
+ {
+ let mut data = client.data.write().await;
+
+ data.insert::<SharedHttp>(Arc::new(http_client));
+ data.insert::<SharedConfig>(Arc::new(config.clone()));
+ }
+
+ 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 register ctrl+c handler!");
+ shard_manager.shutdown_all().await;
+ });
+
+ // run our jobs
+ bot_jobs::dispatch(config)?;
+
+ Ok(client)
+}
diff --git a/crates/bot-commands/Cargo.toml b/crates/bot-commands/Cargo.toml
new file mode 100644
index 0000000..3594c70
--- /dev/null
+++ b/crates/bot-commands/Cargo.toml
@@ -0,0 +1,34 @@
+[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/about.rs b/crates/bot-commands/src/about.rs
new file mode 100644
index 0000000..2e5efae
--- /dev/null
+++ b/crates/bot-commands/src/about.rs
@@ -0,0 +1,44 @@
+use bot_error::Error;
+use bot_http::TeawieClientExt;
+
+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(
+ ctx: &Context,
+ http: &bot_http::Client,
+ command: &CommandInteraction,
+) -> Result<(), Error> {
+ 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)
+ ]);
+
+ if let Some(teawie_url) = http.random_teawie().await? {
+ let footer = CreateEmbedFooter::new("Images courtesy of @sympathytea");
+ embed = embed.image(teawie_url).footer(footer);
+ };
+
+ 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/bot-commands/src/lib.rs b/crates/bot-commands/src/lib.rs
new file mode 100644
index 0000000..79fce17
--- /dev/null
+++ b/crates/bot-commands/src/lib.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/src/command/ping.rs b/crates/bot-commands/src/ping.rs
index 1b1b812..b18a0b6 100644
--- a/src/command/ping.rs
+++ b/crates/bot-commands/src/ping.rs
@@ -1,14 +1,12 @@
-use eyre::Result;
+use bot_error::Error;
+
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");
+pub async fn respond(ctx: &Context, command: &CommandInteraction) -> Result<(), Error> {
let message = CreateInteractionResponseMessage::new().content("Pong!");
let response = CreateInteractionResponse::Message(message);
command.create_response(&ctx, response).await?;
diff --git a/crates/bot-commands/src/track.rs b/crates/bot-commands/src/track.rs
new file mode 100644
index 0000000..1f22d0e
--- /dev/null
+++ b/crates/bot-commands/src/track.rs
@@ -0,0 +1,121 @@
+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
new file mode 100644
index 0000000..57b9a67
--- /dev/null
+++ b/crates/bot-config/Cargo.toml
@@ -0,0 +1,23 @@
+[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-config/src/lib.rs b/crates/bot-config/src/lib.rs
new file mode 100644
index 0000000..0691884
--- /dev/null
+++ b/crates/bot-config/src/lib.rs
@@ -0,0 +1,36 @@
+use std::env;
+
+/// The Discord client's configuration
+#[derive(Clone, Debug)]
+pub struct Config {
+ /// Path to clone a new or use an existing nixpkgs repository
+ pub nixpkgs_path: String,
+ // A comma separated list of nixpkgs branch to track commits for
+ pub nixpkgs_branches: Vec<String>,
+}
+
+impl Config {
+ /// Take in a comma separated list and split it into a [`Vec<String>`]
+ fn split_string_list(branches: &str) -> Vec<String> {
+ branches
+ .split(',')
+ .map(|branch| branch.trim().to_string())
+ .collect()
+ }
+
+ /// Create a new instance of [`Config`] based on variables from the environment
+ ///
+ /// # Errors
+ ///
+ /// Will return [`Err`] if a variable is not found
+ pub fn from_env() -> Result<Self, env::VarError> {
+ let nixpkgs_path = env::var("BOT_NIXPKGS_PATH")?;
+ let nixpkgs_branches_raw = env::var("BOT_NIXPKGS_BRANCHES")?;
+ let nixpkgs_branches = Self::split_string_list(&nixpkgs_branches_raw);
+
+ Ok(Self {
+ nixpkgs_path,
+ nixpkgs_branches,
+ })
+ }
+}
diff --git a/crates/bot-consts/Cargo.toml b/crates/bot-consts/Cargo.toml
new file mode 100644
index 0000000..16d7726
--- /dev/null
+++ b/crates/bot-consts/Cargo.toml
@@ -0,0 +1,23 @@
+[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-consts/src/lib.rs b/crates/bot-consts/src/lib.rs
new file mode 100644
index 0000000..9396da0
--- /dev/null
+++ b/crates/bot-consts/src/lib.rs
@@ -0,0 +1,5 @@
+/// URL to the nixpkgs repository
+pub const NIXPKGS_URL: &str = "https://github.com/NixOS/nixpkgs";
+
+/// The Git remote for upstream nixpkgs in our local copy
+pub const NIXPKGS_REMOTE: &str = "origin";
diff --git a/crates/bot-error/Cargo.toml b/crates/bot-error/Cargo.toml
new file mode 100644
index 0000000..c6f6ed1
--- /dev/null
+++ b/crates/bot-error/Cargo.toml
@@ -0,0 +1,23 @@
+[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
new file mode 100644
index 0000000..f34e60e
--- /dev/null
+++ b/crates/bot-error/src/lib.rs
@@ -0,0 +1 @@
+pub type Error = Box<dyn std::error::Error + Send + Sync>;
diff --git a/crates/bot-http/Cargo.toml b/crates/bot-http/Cargo.toml
new file mode 100644
index 0000000..e451537
--- /dev/null
+++ b/crates/bot-http/Cargo.toml
@@ -0,0 +1,26 @@
+[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.4", default-features = false, features = ["charset", "http2", "rustls-tls", "json"] }
+serde = { version = "1.0.203", 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
new file mode 100644
index 0000000..7822eb8
--- /dev/null
+++ b/crates/bot-http/src/github.rs
@@ -0,0 +1,35 @@
+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/src/http/mod.rs b/crates/bot-http/src/lib.rs
index fa60d67..ab32cd4 100644
--- a/src/http/mod.rs
+++ b/crates/bot-http/src/lib.rs
@@ -1,22 +1,31 @@
+use std::future::Future;
+
+use log::trace;
use serde::de::DeserializeOwned;
-use tracing::trace;
mod github;
+mod model;
+mod teawie;
-pub use github::*;
+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 HttpClientExt {
+pub trait ClientExt {
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>;
+ 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 HttpClientExt for Client {
+impl ClientExt for Client {
+ /// Create the default [`Client`]
fn default() -> Self {
reqwest::Client::builder()
.user_agent(format!(
@@ -27,6 +36,11 @@ impl HttpClientExt for Client {
.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}");
@@ -36,6 +50,11 @@ impl HttpClientExt for Client {
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?;
diff --git a/crates/bot-http/src/model.rs b/crates/bot-http/src/model.rs
new file mode 100644
index 0000000..afd4717
--- /dev/null
+++ b/crates/bot-http/src/model.rs
@@ -0,0 +1,13 @@
+use serde::Deserialize;
+
+/// Bad version of `/repos/{owner}/{repo}/pulls/{pull_number}` for Github's api
+#[derive(Clone, Debug, Deserialize)]
+pub struct PullRequest {
+ pub merge_commit_sha: Option<String>,
+}
+
+/// `/random_teawie` for the teawieAPI
+#[derive(Clone, Debug, Deserialize)]
+pub struct RandomTeawie {
+ pub url: Option<String>,
+}
diff --git a/crates/bot-http/src/teawie.rs b/crates/bot-http/src/teawie.rs
new file mode 100644
index 0000000..ea4f53e
--- /dev/null
+++ b/crates/bot-http/src/teawie.rs
@@ -0,0 +1,24 @@
+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
new file mode 100644
index 0000000..21b0248
--- /dev/null
+++ b/crates/bot-jobs/Cargo.toml
@@ -0,0 +1,29 @@
+[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
new file mode 100644
index 0000000..d65c929
--- /dev/null
+++ b/crates/bot-jobs/src/lib.rs
@@ -0,0 +1,30 @@
+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
new file mode 100644
index 0000000..4d3e214
--- /dev/null
+++ b/crates/bot-jobs/src/repo.rs
@@ -0,0 +1,77 @@
+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
new file mode 100644
index 0000000..c56fc52
--- /dev/null
+++ b/crates/bot/Cargo.toml
@@ -0,0 +1,26 @@
+[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.3"
+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/src/main.rs b/crates/bot/src/main.rs
new file mode 100644
index 0000000..390e79b
--- /dev/null
+++ b/crates/bot/src/main.rs
@@ -0,0 +1,10 @@
+#[tokio::main]
+async fn main() -> Result<(), bot_error::Error> {
+ dotenvy::dotenv().ok();
+ env_logger::try_init()?;
+
+ let mut client = bot_client::get().await?;
+ client.start().await?;
+
+ Ok(())
+}
diff --git a/crates/git-tracker/Cargo.toml b/crates/git-tracker/Cargo.toml
new file mode 100644
index 0000000..60baa41
--- /dev/null
+++ b/crates/git-tracker/Cargo.toml
@@ -0,0 +1,27 @@
+[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"
+
+publish = false
+
+[dependencies]
+git2 = { workspace = true }
+log = { workspace = true }
+thiserror = "1.0.61"
+
+[lints.rust]
+async_fn_in_trait = "allow"
+unsafe_code = "forbid"
+
+[lints.clippy]
+complexity = "warn"
+correctness = "deny"
+pedantic = "warn"
+perf = "warn"
+style = "warn"
+suspicious = "deny"
diff --git a/crates/git-tracker/src/lib.rs b/crates/git-tracker/src/lib.rs
new file mode 100644
index 0000000..cb0907b
--- /dev/null
+++ b/crates/git-tracker/src/lib.rs
@@ -0,0 +1,4 @@
+//! A library that helps you track commits and branches in a Git repository
+
+mod tracker;
+pub use tracker::{Error, Tracker};
diff --git a/crates/git-tracker/src/tracker.rs b/crates/git-tracker/src/tracker.rs
new file mode 100644
index 0000000..e26f82d
--- /dev/null
+++ b/crates/git-tracker/src/tracker.rs
@@ -0,0 +1,109 @@
+use std::path::Path;
+
+use git2::{Branch, BranchType, Commit, ErrorCode, Oid, Reference, Repository};
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ #[error("libgit2 error")]
+ Git(#[from] git2::Error),
+ #[error("Repository path not found at `{0}`")]
+ RepositoryPathNotFound(String),
+}
+
+/// Helper struct for tracking Git objects
+pub struct Tracker {
+ repository: Repository,
+}
+
+impl Tracker {
+ /// Create a new [`Tracker`] using the repository at [`path`]
+ ///
+ /// # Errors
+ ///
+ /// Will return [`Err`] if the repository can not be opened
+ pub fn from_path(path: &str) -> Result<Self, Error> {
+ let repository_path = Path::new(path);
+ if repository_path.exists() {
+ let repository = Repository::open(repository_path)?;
+ Ok(Self { repository })
+ } else {
+ Err(Error::RepositoryPathNotFound(path.to_string()))
+ }
+ }
+
+ /// Finds a branch of name [`name`]
+ ///
+ /// # Errors
+ ///
+ /// Will return [`Err`] if the branch cannot be found locally
+ pub fn branch_by_name(&self, name: &str) -> Result<Branch, Error> {
+ Ok(self.repository.find_branch(name, BranchType::Remote)?)
+ }
+
+ /// Finds a commit with a SHA match [`sha`]
+ ///
+ /// # Errors
+ ///
+ /// Will return [`Err`] if [`sha`] cannot be converted an [`Oid`] or
+ /// a commit matching it cannot be found
+ pub fn commit_by_sha(&self, sha: &str) -> Result<Commit, Error> {
+ let oid = Oid::from_str(sha)?;
+ let commit = self.repository.find_commit(oid)?;
+
+ Ok(commit)
+ }
+
+ /// Check if [`Reference`] [`ref`] contains [`Commit`] [`commit`]
+ ///
+ /// # Errors
+ ///
+ /// Will return [`Err`] if the reference cannot be resolved to a commit or the descendants
+ /// of the reference cannot be resolved
+ pub fn ref_contains_commit(
+ &self,
+ reference: &Reference,
+ commit: &Commit,
+ ) -> Result<bool, Error> {
+ let head = reference.peel_to_commit()?;
+
+ // NOTE: we have to check this as `Repository::graph_descendant_of()` (like the name says)
+ // only finds *descendants* of it's parent commit, and will not tell us if the parent commit
+ // *is* the child commit. i have no idea why i didn't think of this, but that's why this
+ // comment is here now
+ let is_head = head.id() == commit.id();
+
+ let has_commit = self
+ .repository
+ .graph_descendant_of(head.id(), commit.id())?;
+
+ Ok(has_commit || is_head)
+ }
+
+ /// Check if a [`Branch`] named [`branch_name`] has a commit with the SHA [`commit_sha`]
+ ///
+ /// # Errors
+ ///
+ /// Will return [`Err`] if the commit SHA cannot be resolved to an object id, the branch name cannot
+ /// be resolved to a branch, or the descendants of the resolved branch cannot be resolved
+ pub fn branch_contains_sha(&self, branch_name: &str, commit_sha: &str) -> Result<bool, Error> {
+ let commit = match self.commit_by_sha(commit_sha) {
+ Ok(commit) => commit,
+ Err(why) => {
+ // NOTE: we assume commits not found are just not in the branch *yet*, not an error
+ // this is because github decides to report merge commit shas for unmerged PRs...yeah
+ if let Error::Git(git_error) = &why {
+ if git_error.code() == ErrorCode::NotFound {
+ return Ok(false);
+ }
+ }
+
+ return Err(why);
+ }
+ };
+
+ let branch = self.branch_by_name(branch_name)?;
+ let has_pr = self.ref_contains_commit(&branch.into_reference(), &commit)?;
+
+ Ok(has_pr)
+ }
+}
diff --git a/flake.lock b/flake.lock
index aa829b4..49dca02 100644
--- a/flake.lock
+++ b/flake.lock
@@ -46,9 +46,10 @@
"type": "github"
},
"original": {
- "id": "nixpkgs",
+ "owner": "NixOS",
"ref": "nixpkgs-unstable",
- "type": "indirect"
+ "repo": "nixpkgs",
+ "type": "github"
}
},
"root": {
diff --git a/flake.nix b/flake.nix
index 1ced965..fa7af57 100644
--- a/flake.nix
+++ b/flake.nix
@@ -2,7 +2,8 @@
description = "A Discord app for tracking nixpkgs PRs";
inputs = {
- nixpkgs.url = "nixpkgs/nixpkgs-unstable";
+ nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+
fenix = {
url = "github:nix-community/fenix";
inputs = {
@@ -10,6 +11,7 @@
rust-analyzer-src.follows = "";
};
};
+
flake-checks.url = "github:getchoo/flake-checks";
};
@@ -54,14 +56,11 @@
...
}: let
inputsFrom = [self.packages.${system}.nixpkgs-tracker-bot];
- RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}";
in {
default = pkgs.mkShell {
- inherit inputsFrom RUST_SRC_PATH;
- };
+ inherit inputsFrom;
+ RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}";
- full = pkgs.mkShell {
- inherit inputsFrom RUST_SRC_PATH;
packages = [
pkgs.clippy
pkgs.rustfmt
@@ -75,6 +74,14 @@
self.formatter.${system}
];
};
+
+ ci = pkgs.mkShell {
+ inherit inputsFrom;
+ packages = [
+ pkgs.clippy
+ pkgs.rustfmt
+ ];
+ };
});
formatter = forAllSystems (pkgs: pkgs.alejandra);
@@ -89,12 +96,10 @@
}: let
packages = self.packages.${system};
- mkStaticForArch = arch:
- pkgs.callPackage ./nix/static.nix {
- inherit arch;
- inherit (packages) nixpkgs-tracker-bot;
- fenix = fenix.packages.${system};
- };
+ mkStaticWith = pkgs.callPackage ./nix/static.nix {
+ inherit (packages) nixpkgs-tracker-bot;
+ fenix = fenix.packages.${system};
+ };
containerWith = nixpkgs-tracker-bot: let
arch = nixpkgs-tracker-bot.stdenv.hostPlatform.ubootArch;
@@ -112,8 +117,8 @@
default = packages.nixpkgs-tracker-bot;
- static-x86_64 = mkStaticForArch "x86_64";
- static-arm64 = mkStaticForArch "aarch64";
+ static-x86_64 = mkStaticWith {arch = "x86_64";};
+ static-arm64 = mkStaticWith {arch = "aarch64";};
container-x86_64 = containerWith packages.static-x86_64;
container-arm64 = containerWith packages.static-arm64;
diff --git a/nix/module.nix b/nix/module.nix
index ec9da78..3d23ead 100644
--- a/nix/module.nix
+++ b/nix/module.nix
@@ -47,16 +47,26 @@ in {
${getExe cfg.package}
'';
+ environment = {
+ # using `/var/lib/private` as we have `DynamicUser` enabled
+ BOT_NIXPKGS_PATH = "/var/lib/private/${config.systemd.services.nixpkgs-tracker-bot.serviceConfig.StateDirectory}/nixpkgs";
+ };
+
serviceConfig = {
Type = "simple";
Restart = "on-failure";
EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
- # hardening
+ StateDirectory = "nixpkgs-tracker-bot";
+
+ # hardening settings
DynamicUser = true;
+ LockPersonality = true;
+ MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
+ PrivateIPC = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
@@ -66,16 +76,16 @@ in {
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
+ ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictNamespaces = "uts ipc pid user cgroup";
+ RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
- "~@resources"
- "~@privileged"
];
- Umask = "0007";
+ UMask = "0077";
};
};
};
diff --git a/nix/package.nix b/nix/package.nix
index 2802233..778fa27 100644
--- a/nix/package.nix
+++ b/nix/package.nix
@@ -1,6 +1,8 @@
{
lib,
rustPlatform,
+ openssl,
+ pkg-config,
version,
lto ? true,
optimizeSize ? false,
@@ -12,16 +14,16 @@ rustPlatform.buildRustPackage {
src = lib.fileset.toSource {
root = ../.;
fileset = lib.fileset.unions [
- ../src
+ (lib.fileset.gitTracked ../crates)
../Cargo.toml
../Cargo.lock
];
};
- cargoLock = {
- lockFile = ../Cargo.lock;
- allowBuiltinFetchGit = true;
- };
+ cargoLock.lockFile = ../Cargo.lock;
+
+ nativeBuildInputs = [pkg-config];
+ buildInputs = [openssl];
env = let
toRustFlags = lib.mapAttrs' (
diff --git a/nix/static.nix b/nix/static.nix
index f79de47..c5e3c57 100644
--- a/nix/static.nix
+++ b/nix/static.nix
@@ -1,16 +1,15 @@
{
lib,
- arch,
- nixpkgs-tracker-bot,
fenix,
pkgsCross,
+ nixpkgs-tracker-bot,
}: let
- crossTargetFor = with pkgsCross; {
+ crossPkgsFor = with pkgsCross; {
x86_64 = musl64.pkgsStatic;
aarch64 = aarch64-multiplatform;
};
- rustcTargetFor = lib.mapAttrs (lib.const (pkgs: pkgs.stdenv.hostPlatform.rust.rustcTarget)) crossTargetFor;
+ rustcTargetFor = lib.mapAttrs (lib.const (pkgs: pkgs.stdenv.hostPlatform.rust.rustcTarget)) crossPkgsFor;
rustStdFor = lib.mapAttrs (lib.const (rustcTarget: fenix.targets.${rustcTarget}.stable.rust-std)) rustcTargetFor;
toolchain = with fenix;
@@ -26,9 +25,11 @@
lib.genAttrs ["cargo" "rustc"] (lib.const toolchain)
))
)
- crossTargetFor;
+ crossPkgsFor;
in
- nixpkgs-tracker-bot.override {
- rustPlatform = crossPlatformFor.${arch};
- optimizeSize = true;
- }
+ {arch}:
+ nixpkgs-tracker-bot.override {
+ rustPlatform = crossPlatformFor.${arch};
+ inherit (crossPkgsFor.${arch}) openssl;
+ optimizeSize = true;
+ }
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/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/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(())
-}