first commit

This commit is contained in:
ace 2021-07-31 23:04:12 +03:00
commit c7d2b176f6
No known key found for this signature in database
GPG Key ID: 2E47CC17BA7F8CF0
9 changed files with 1992 additions and 0 deletions

5
.dockerignore Normal file
View File

@ -0,0 +1,5 @@
**
!Cargo.toml
!Cargo.lock
!src
!docker

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
/data
config.yaml

1692
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

21
Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "github-release-bot"
version = "0.1.0"
authors = ["ace <ace@0xace.cc>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.8"
octocrab = "0.8"
tokio = { version = "1", features = ["full"] }
futures = "0.3"
clap = "3.0.0-beta.2"
#telegram-bot = { git = "https://github.com/telegram-rs/telegram-bot", rev = "07a9f9a1c76eaab2259bdc6241691187a46d69d1" }
telegram-bot = { git = "https://github.com/telegram-rs/telegram-bot", rev = "65ad5cfd578e9a1260ce6daac714eb2153c0bec7" }
log = "0.4"
stderrlog = "0.5"
rand = "0.8"
chrono ="0.4"

15
Makefile Normal file
View File

@ -0,0 +1,15 @@
VERSION=0.1.0
IMAGE=github-release-bot
ifneq ($(REGISTRY),)
_REGISTRY =$(REGISTRY)/
endif
.PHONY: build push all
build:
docker build -t $(_REGISTRY)$(IMAGE):$(VERSION) -f docker/Dockerfile .
push:
docker push $(_REGISTRY)$(IMAGE):$(VERSION)
all: build push

61
README.md Normal file
View File

@ -0,0 +1,61 @@
# GitHub release bot
Telegram bot for GitHub releases notification. Bot simply fetches latest release and send notification.
**Warning**: GitHub will ban you with small interval and spread time. Use adequate interval and spread time or use TOR.
Interval - set how often check GitHub for new releases in seconds. Default - 28800 seconds (8 hours).
Spread - time in seconds for random offset calculation per repo within interval.
Default 600 seconds (10 minutes). Offset calculated as RANDOM(u64) % SPREAD.
With default values bot will launch main loop check every 8 hours and get latest release within (RANDOM(u64) % 600) seconds per repo.
## Example config.yaml
```yaml
- owner: go-gitea
repo: gitea
- owner: kubernetes-sigs
repo: kubespray
```
## Binary
### Build binary
```ShellSession
cargo build --release
```
### Run binary
```ShellSession
github-release-bot \
--token ${TOKEN} --chatid ${CHATID} \
--config ${CONFIG} --datadir ${DATADIR} \
--interval ${INTERVAL} --spread ${SPREAD}
```
### Binary params
- token - required, Telegram token
- chatid - required, Telegram chat id
- config - file required, default config file name "config.yaml"
- interval - optional, default 28800 seconds (8 hours)
- spread - optional, default 600 seconds (10 minutes)
- datadir - optional, dir for storing files with latest releases, default "data"
## Docker
### Docker build
```ShellSession
make build
```
### Docker run
```ShellSession
docker run -d -v ./config.yaml:/opt/config.yaml -v ./data:/opt/data registry.0xace.cc/ghp/github-release-bot:latest \
-e TOKEN=${TOKEN} -e CHATID=${CHATID} \
-e CONFIG=${CONFIG} -e DATADIR=${DATADIR} \
-e INTERVAL=${INTERVAL} -e SPREAD=${SPREAD}
```
### Docker params
OS ENV used to pass arguments:
- TOKEN - required, Telegram token
- CHATID - required, Telegram chat id
- CONFIG - file required, default config file name "config.yaml"
- INTERVAL - optional, default 28800 seconds (8 hours)
- SPREAD - optional, default 600 seconds (10 minutes)
- DATADIR - optional, dir for storing files with latest releases, default "data"

11
docker/Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM ekidd/rust-musl-builder as builder
ADD --chown=rust:rust . ./
RUN cargo build --release
FROM alpine:latest
COPY --from=builder /home/rust/src/target/x86_64-unknown-linux-musl/release/github-release-bot /opt/github-release-bot
COPY ./docker/entrypoint.sh /opt/entrypoint.sh
RUN chmod +x /opt/entrypoint.sh
WORKDIR /opt
ENTRYPOINT ["/opt/entrypoint.sh"]

20
docker/entrypoint.sh Normal file
View File

@ -0,0 +1,20 @@
#!/bin/sh
set -e
if [[ -z "${TOKEN}" ]]; then
echo "Must set Telegram token"
exit 1
fi
if [[ -z "${CHATID}" ]]; then
echo "Must set Telegram chat id"
exit 1
fi
INTERVAL="${INTERVAL:-28800}"
SPREAD="${SPREAD:-600}"
CONFIG="${CONFIG:-config.yaml}"
DATADIR="${DATADIR:-data}"
/opt/github-release-bot --token ${TOKEN} --chatid ${CHATID} --interval ${INTERVAL} --spread ${SPREAD} --config ${CONFIG} --datadir ${DATADIR}

164
src/main.rs Normal file
View File

@ -0,0 +1,164 @@
use serde_yaml;
use serde::{Deserialize, Serialize};
use octocrab;
use telegram_bot::*;
use clap::Clap;
use std::future::Future;
use std::time::Duration;
use futures;
use tokio;
use log::*;
use chrono::prelude::*;
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Repo {
owner: String,
repo: String
}
impl Repo {
async fn init(&mut self) {
tokio::time::sleep(Duration::from_millis(0)).await;
let _ = create_repo_files(&self.owner, &self.repo).await;
}
async fn resolve(&mut self) {
let opts: Opts = Opts::parse();
let random_spred = rand::random::<u64>();
println!("{} - LOG - {}/{} will be checked in {} secs ({} interval and {} random spread)",
Local::now().trunc_subsecs(0).to_rfc3339(), &self.owner, &self.repo, opts.interval+(random_spred % opts.spread), opts.interval, random_spred % opts.spread);
tokio::time::sleep(Duration::from_secs(opts.interval+(random_spred % opts.spread))).await;
let _res = github_get_latest_release(&self.owner, &self.repo).await;
}
// fn print_result(&self) {
// println!("{}, {}", self.owner, self.repo);
// }
}
#[derive(Clap)]
// #[clap(version = "1.0")]
struct Opts {
/// Config file
#[clap(long, default_value = "config.yaml")]
config: String,
/// Data dir
#[clap(long, default_value = "data")]
datadir: String,
/// Telegram token
#[clap(long)]
token: String,
/// Check interval
#[clap(long, default_value = "28800")]
interval: u64,
/// Spread interval
#[clap(long, default_value = "600")]
spread: u64,
/// Telegram chat id
#[clap(long, allow_hyphen_values(true))]
chatid: i64,
/// Verbose level, accept multiple occurrences
#[clap(short, long, parse(from_occurrences))]
verbose: usize,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let opts: Opts = Opts::parse();
stderrlog::new()
.module(module_path!())
.verbosity(opts.verbose+1)
.timestamp(stderrlog::Timestamp::Second)
.quiet(false)
.init()
.unwrap();
println!("{} - LOG - Config file name: {}", Local::now().trunc_subsecs(0).to_rfc3339(), opts.config);
debug!("Using token: {}", opts.token);
println!("{} - LOG - Using chat id: {}", Local::now().trunc_subsecs(0).to_rfc3339(), opts.chatid);
println!("{} - LOG - Using check interval {} secs + random spread from {}", Local::now().trunc_subsecs(0).to_rfc3339(), opts.interval, opts.spread);
let config: Vec<Repo> = get_config(&opts.config).await?;
let items: Vec<_> = config.iter().map(|x| Repo { owner: x.owner.trim().to_string(), repo: x.repo.trim().to_string() }).collect();
let _items = join_parallel(items.into_iter().map(|mut item| async {
item.init().await;
item
}))
.await;
loop {
let items: Vec<_> = config.iter().map(|x| Repo { owner: x.owner.trim().to_string(), repo: x.repo.trim().to_string() }).collect();
let _items = join_parallel(items.into_iter().map(|mut item| async {
item.resolve().await;
item
}))
.await;
}
}
async fn join_parallel<T: Send + 'static>(
futs: impl IntoIterator<Item = impl Future<Output = T> + Send + 'static>,
) -> Vec<T> {
let tasks: Vec<_> = futs.into_iter().map(tokio::spawn).collect();
futures::future::join_all(tasks)
.await
.into_iter()
.map(Result::unwrap)
.collect()
}
async fn get_config(filename: impl AsRef<std::path::Path>) -> Result<Vec<Repo>, Box<dyn std::error::Error>> {
let config = std::fs::File::open(filename)?;
let contents: Vec<Repo> = serde_yaml::from_reader(config)?;
Ok(contents)
}
async fn create_repo_files(owner: &String, repo: &String) -> Result<(), Box<dyn std::error::Error>> {
debug!("Create repo file {}-{}", owner, repo);
let opts: Opts = Opts::parse();
let _repofile = tokio::fs::OpenOptions::new().write(true).create(true).open(opts.datadir.to_string() + "/" + &owner.trim().to_string() + "-" + &repo.trim().to_string()).await?;
Ok(())
}
async fn read_file(filename: impl AsRef<std::path::Path>) -> Result<String, Box<dyn std::error::Error>>{
let contents = tokio::fs::read_to_string(filename).await?;
Ok(contents)
}
async fn overwrite_repo_file(filename: impl AsRef<std::path::Path>, release_tag: &String) -> Result<(), Box<dyn std::error::Error>> {
let mut file = tokio::fs::File::create(filename).await?;
tokio::io::AsyncWriteExt::write_all(&mut file, release_tag.as_bytes()).await?;
Ok(())
}
async fn telegram_notify(_release_tag: &String, release_url: &String) -> Result<(), Box<dyn std::error::Error>> {
let opts: Opts = Opts::parse();
let api = Api::new(opts.token);
let chat = ChatId::new(opts.chatid);
api.send(chat.text(release_url)).await?;
Ok(())
}
async fn github_get_latest_release(owner: &String, repo: &String) -> octocrab::Result<(), Box<dyn std::error::Error>> {
let opts: Opts = Opts::parse();
let release = octocrab::instance()
.repos(owner, repo)
.releases()
.get_latest()
.await?;
let version_in_file = read_file(opts.datadir.to_string() + "/" + &owner.trim().to_string() + "-" + &repo.trim().to_string()).await?;
match version_in_file.to_string() == release.tag_name.to_string() {
true => {
info!("{}/{}: current known version is {:?}", owner, repo, version_in_file.to_string());
println!("{} - LOG - {}/{}: known version matches with latest release {}", Local::now().trunc_subsecs(0).to_rfc3339(), owner, repo, release.tag_name.to_string());
}
_ => {
info!("{}/{}: current known version is {:?}", owner, repo, version_in_file.to_string());
println!("{} - LOG - {}/{}: known version does not match with latest release {}. Sending new release URL {}", Local::now().trunc_subsecs(0).to_rfc3339(), owner, repo, release.tag_name.to_string(), release.html_url);
let _ = overwrite_repo_file(opts.datadir.to_string() + "/" + &owner.to_string() + "-" + &repo.to_string(), &release.tag_name.to_string()).await?;
let _ = telegram_notify(&release.tag_name.to_string(), &release.html_url.to_string()).await?;
}
};
Ok(())
}