use std::sync::Arc; use tokio::sync::Mutex; use tokio_postgres::{Client, Row}; use crate::sec; use super::db; #[derive(Clone)] pub struct Users(Arc>); impl Users { pub fn new(client: Arc>) -> Self { Self(client) } pub async fn create_user(&self, u: User) -> Result { let row = self.0.lock().await.query_one( "insert into users (id, username, host, display_name, password_hash, email) values ($1, $2, $3, $4, $5, $6) returning (id, username, host, display_name, password_hash, email, avatar_uri, bio)", &[&sec::new_id(), &u.username, &u.host, &u.display_name, &u.password_hash, &u.email], ).await?; Ok(User::from(&row)) } pub async fn user_stats(&self, by: UserSelect) -> Result { let (clause, param) = by.into(); let rows = self .0 .lock() .await .query( format!(r#"select count(follows.*) as following, count(followed.*) as followers from users left join follows on follows.user_id = users.id left join follows followed on followed.follows_id = users.id where {}"#, clause).as_str(), &[¶m], ) .await?; Ok(rows.first().ok_or(db::DBError::NotFound)?.into()) } pub async fn user(&self, by: UserSelect) -> Result { let (clause, param) = by.into(); let rows = self .0 .lock() .await .query( format!( "select id, username, host, display_name, password_hash, email, avatar_uri, bio from users where {}", clause, ) .as_str(), &[¶m], ) .await?; if let Some(row) = rows.first() && rows.len() == 1 { Ok(User::from(row)) } else { Err(db::DBError::NotFound) } } } #[derive(Debug, Clone)] pub struct User { pub id: String, pub username: String, pub host: Option, pub display_name: Option, pub password_hash: String, pub email: String, pub avatar_uri: Option, pub bio: Option, } impl From<&Row> for User { fn from(row: &Row) -> Self { Self { id: row.get("id"), username: row.get("username"), host: row.get("host"), display_name: row.get("display_name"), password_hash: row.get("password_hash"), email: row.get("email"), avatar_uri: row.get("avatar_uri"), bio: row.get("bio"), } } } pub enum UserSelect { ID(String), Username(String), FullUsername(String), } impl From for UserSelect { fn from(username: String) -> Self { if !username.contains("@") { Self::Username(username) } else { Self::FullUsername(username) } } } impl Into<(String, String)> for UserSelect { fn into(self) -> (String, String) { let where_param: String; let where_clause = match self { UserSelect::ID(id) => { where_param = id; "users.id = $1" } UserSelect::Username(username) => { where_param = username; "users.username = $1" } UserSelect::FullUsername(full) => { where_param = full; "(users.username || '@' || users.host) = $1" } }; (where_clause.to_owned(), where_param) } } #[derive(Debug, Clone)] pub struct UserStats { pub post_count: i64, pub following: i64, pub followers: i64, } impl From<&Row> for UserStats { fn from(row: &Row) -> Self { Self { post_count: 100, following: row.get("following"), followers: row.get("followers"), } } }