/* * Admin Tools - Gitea admin tools to deal with spammers. * Copyright (C) 2022 Aravinth Manivannan * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::str::FromStr; use std::env; use std::error::Error; use tracing::*; use reqwest::Client; use serde::{Deserialize, Serialize}; use sqlx::sqlite::SqlitePool; use sqlx::ConnectOptions; use url::Url; #[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct User { id: i64, login: String, created: String, is_admin: bool, } pub struct Gitea { base_url: Url, admin_auth: String, client: Client, } type MyResult = Result>; impl Gitea { async fn is_admin(&self) -> MyResult { let mut url = self.base_url.clone(); url.set_path("/api/v1/user"); let user: User = self .client .get(url) .bearer_auth(&self.admin_auth) .send() .await? .json() .await?; Ok(user.is_admin) } pub fn new(base_url: Url, admin_auth: String) -> Self { Self { base_url, admin_auth, client: Client::new(), } } pub fn from_env() -> MyResult { let admin_auth = env::var("AUTH")?; let base_url = env::var("GITEA_URL")?; let base_url = Url::parse(&base_url)?; Ok(Self::new(base_url, admin_auth)) } pub async fn list_all_users(&self, page: usize, limit: usize) -> MyResult> { let mut url = self.base_url.clone(); url.set_path("/api/v1/admin/users"); url.set_query(Some(&format!("page={page}&limit={limit}"))); let u: Vec = self .client .get(url) .bearer_auth(&self.admin_auth) .send() .await? .json() .await?; Ok(u) } } #[tokio::main] async fn main() -> MyResult<()> { if env::var("RUST_LOG").is_err() { env::set_var("RUST_LOG", "info"); } pretty_env_logger::init(); let gitea = Gitea::from_env()?; info!("Starting run. Gitea instance: {}", &gitea.base_url); if !gitea.is_admin().await? { error!("User isn't admin"); } let db = DB::from_env().await?; let mut page = 0; let limit = 50; let mut new_users = 0; loop { let users = gitea.list_all_users(page, limit).await?; if users.len() == 0 { break; } page += 1; for u in users.iter() { if !db.user_exists(&u.login).await? { warn!("New user {} created on {}", u.login, u.created); db.add_user(u).await?; new_users += 1; } } } if new_users > 0 { info!("New users: {new_users}"); } else { info!("No new users"); } Ok(()) } pub struct DB { db: SqlitePool, } impl DB { async fn new(url: &str) -> MyResult { let mut connect_options = sqlx::sqlite::SqliteConnectOptions::from_str(url)?; connect_options.disable_statement_logging(); let pool = sqlx::sqlite::SqlitePoolOptions::new() .connect_with(connect_options) .await?; Ok(Self { db: pool }) } async fn from_env() -> MyResult { let db_url = env::var("DATABASE_URL")?; Self::new(&db_url).await } async fn user_exists(&self, login: &str) -> MyResult { match sqlx::query!("SELECT ID FROM admin_users WHERE login = $1", login) .fetch_one(&self.db) .await { Ok(_) => Ok(true), Err(sqlx::Error::RowNotFound) => Ok(false), Err(e) => Err(e.into()), } } async fn add_user(&self, user: &User) -> MyResult<()> { sqlx::query!( "INSERT INTO admin_users (ID, login, is_admin, created) VALUES ($1, $2, $3, $4);", user.id, user.login, user.is_admin, user.created ) .execute(&self.db) .await?; Ok(()) } }