diff --git a/migrations/20221115201707_admin_users.sql b/migrations/20221115201707_admin_users.sql new file mode 100644 index 0000000..3994b11 --- /dev/null +++ b/migrations/20221115201707_admin_users.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS admin_users( + is_admin BOOLEAN NOT NULL, + ID INTEGER PRIMARY KEY NOT NULL, + login TEXT NOT NULL UNIQUE, + created TEXT NOT NULL +); diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7ad2be6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,182 @@ +/* + * 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(()) + } +}