use chrono::NaiveDateTime; use iter_tools::Itertools; use std::{collections::HashMap, sync::Arc}; use tokio::sync::Mutex; use anyhow::Context; use rusqlite::Connection; use crate::domain::intro_tool::{ models::guild::{ self, AddIntroToGuildError, AddIntroToGuildRequest, AddIntroToUserRequest, Channel, ChannelName, CreateChannelError, CreateChannelRequest, CreateGuildError, CreateGuildRequest, CreateUserError, CreateUserRequest, GetChannelError, GetGuildError, GetIntroError, GetUserError, Guild, GuildId, GuildRef, Intro, IntroId, User, UserName, }, ports::IntroToolRepository, }; #[derive(Clone)] pub struct Sqlite { conn: Arc>, } impl Sqlite { pub fn new(path: &str) -> rusqlite::Result { Ok(Self { conn: Arc::new(Mutex::new(Connection::open(path)?)), }) } } impl IntroToolRepository for Sqlite { async fn get_guild(&self, guild_id: GuildId) -> Result { let guild = { let conn = self.conn.lock().await; let mut query = conn .prepare( " select Guild.id, Guild.name, Guild.sound_delay from Guild where Guild.id = :guild_id ", ) .context("failed to prepare query")?; query .query_row(&[(":guild_id", &guild_id.to_string())], |row| { Ok(Guild::new( row.get::<_, u64>(0)?.into(), row.get(1)?, row.get(2)?, row.get::<_, u64>(0)?.into(), )) }) .context("failed to query row")? }; Ok(guild .with_users(self.get_guild_users(guild_id).await?) .with_channels(self.get_guild_channels(guild_id).await?)) } async fn get_guilds(&self) -> Result, GetGuildError> { let conn = self.conn.lock().await; let mut query = conn .prepare( " SELECT Guild.id, Guild.name, Guild.sound_delay FROM Guild LEFT JOIN UserGuild ON Guild.id = UserGuild.guild_id LEFT JOIN User ON User.username = UserGuild.username ", ) .context("failed to prepare query")?; let guilds = query .query_map([], |row| { Ok(GuildRef::new( row.get::<_, u64>(0)?.into(), row.get(1)?, row.get(2)?, row.get::<_, u64>(0)?.into(), )) }) .context("failed to map prepared query")? .collect::>() .context("failed to fetch guild user rows")?; Ok(guilds) } async fn get_guild_count(&self) -> Result { let conn = self.conn.lock().await; let mut query = conn .prepare( " select count(*) from Guild ", ) .context("failed to prepare query")?; Ok(query .query_row([], |row| row.get::<_, usize>(0)) .context("failed to query row")?) } async fn get_guild_users(&self, guild_id: GuildId) -> Result, GetUserError> { let conn = self.conn.lock().await; let mut query = conn .prepare( " SELECT User.username AS name, User.api_key, User.api_key_expires_at, User.discord_token, User.discord_token_expires_at FROM UserGuild LEFT JOIN User ON User.username = UserGuild.username WHERE UserGuild.guild_id = :guild_id ", ) .context("failed to prepare query")?; let users = query .query_map(&[(":guild_id", &guild_id.to_string())], |row| { Ok(User::new( UserName::from(row.get::<_, String>(0)?), row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, )) }) .context("failed to map prepared query")? .collect::>() .context("failed to fetch guild user rows")?; Ok(users) } async fn get_guild_channels(&self, guild_id: GuildId) -> Result, GetChannelError> { let conn = self.conn.lock().await; let mut query = conn .prepare( " SELECT Channel.name FROM Channel WHERE Channel.guild_id = :guild_id ORDER BY Channel.name DESC ", ) .context("failed to prepare query")?; let channels = query .query_map(&[(":guild_id", &guild_id.to_string())], |row| { Ok(Channel::new(row.get::<_, String>(0)?.into())) }) .context("failed to map prepared query")? .collect::>() .context("failed to fetch guild channel rows")?; Ok(channels) } async fn get_guild_intros(&self, guild_id: GuildId) -> Result, GetIntroError> { let conn = self.conn.lock().await; let mut query = conn .prepare( " SELECT Intro.id, Intro.name, Intro.filename FROM Intro WHERE Intro.guild_id = :guild_id ", ) .context("failed to prepare query")?; let intros = query .query_map(&[(":guild_id", &guild_id.to_string())], |row| { Ok(Intro::new( row.get::<_, i32>(0)?.into(), row.get(1)?, row.get(2)?, )) }) .context("failed to map prepared query")? .collect::>() .context("failed to fetch guild intro rows")?; Ok(intros) } async fn get_user(&self, username: impl AsRef) -> Result { let user = { let conn = self.conn.lock().await; let mut query = conn .prepare( " SELECT username AS name, api_key, api_key_expires_at, discord_token, discord_token_expires_at FROM User WHERE username = :username ", ) .context("failed to prepare query")?; query .query_row(&[(":username", username.as_ref())], |row| { Ok(User::new( UserName::from(row.get::<_, String>(0)?), row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?, )) }) .context("failed to query row")? }; let guilds = self .get_user_guilds(username.as_ref()) .await .map_err(Box::new)?; let mut intros = HashMap::new(); for guild in guilds { intros.extend( self.get_user_channel_intros(username.as_ref(), guild.id()) .await?, ); } Ok(user.with_channel_intros(intros)) } async fn get_user_channel_intros( &self, username: impl AsRef, guild_id: GuildId, ) -> Result>, GetIntroError> { let conn = self.conn.lock().await; struct ChannelIntro { channel_name: ChannelName, intro: Intro, } let mut query = conn .prepare( " SELECT Intro.id, Intro.name, Intro.filename, UI.channel_name FROM Intro LEFT JOIN UserIntro UI ON UI.intro_id = Intro.id WHERE UI.username = ?1 AND UI.guild_id = ?2 ", ) .context("failed to prepare query")?; let intros = query .query_map([username.as_ref(), &guild_id.to_string()], |row| { Ok(ChannelIntro { channel_name: ChannelName::from(row.get::<_, String>(3)?), intro: Intro::new(row.get::<_, i32>(0)?.into(), row.get(1)?, row.get(2)?), }) }) .context("failed to map prepared query")? .collect::, _>>() .context("failed to fetch user channel intro rows")?; let intros = intros .into_iter() .map(|intro| ((guild_id, intro.channel_name), intro.intro)) .into_group_map(); Ok(intros) } async fn get_user_guilds( &self, username: impl AsRef, ) -> Result, GetGuildError> { let conn = self.conn.lock().await; let mut query = conn .prepare( " SELECT Guild.id, Guild.name, Guild.sound_delay FROM Guild LEFT JOIN UserGuild ON Guild.id = UserGuild.guild_id LEFT JOIN User ON User.username = UserGuild.username WHERE User.username = :username ", ) .context("failed to prepare query")?; let guilds = query .query_map(&[(":username", username.as_ref())], |row| { Ok(GuildRef::new( row.get::<_, u64>(0)?.into(), row.get(1)?, row.get(2)?, row.get::<_, u64>(0)?.into(), )) }) .context("failed to map prepared query")? .collect::>() .context("failed to fetch guild user rows")?; Ok(guilds) } async fn get_user_from_api_key(&self, api_key: &str) -> Result { let username = { let conn = self.conn.lock().await; let mut query = conn .prepare( " SELECT username AS name FROM User WHERE api_key = :api_key ", ) .context("failed to prepare query")?; query .query_row(&[(":api_key", api_key)], |row| { Ok(UserName::from(row.get::<_, String>(0)?)) }) .context("failed to query row")? }; self.get_user(username).await } async fn set_user_api_key( &self, username: &str, api_key: &str, expires_at: NaiveDateTime, ) -> Result<(), GetUserError> { let conn = self.conn.lock().await; conn.execute( " UPDATE User SET api_key = ?1, api_key_expires_at = ?2 WHERE username = ?3 ", [api_key, &expires_at.to_string(), username], ) .context("failed to update user api key")?; Ok(()) } async fn set_user_intro( &self, req: AddIntroToUserRequest, ) -> Result<(), guild::AddIntroToUserError> { let conn = self.conn.lock().await; conn.execute( " DELETE FROM UserIntro WHERE username = ?1 AND guild_id = ?2 AND channel_name = ?3 ", [ &req.user.to_string(), &req.guild_id.to_string(), &req.channel_name.to_string(), ], ) .context("failed to delete user intros")?; conn.execute( " INSERT INTO UserIntro (username, guild_id, channel_name, intro_id) VALUES (?1, ?2, ?3, ?4)", [ &req.user.to_string(), &req.guild_id.to_string(), &req.channel_name.to_string(), &req.intro_id.to_string(), ], ) .context("failed to insert user intro")?; Ok(()) } async fn create_guild(&self, req: CreateGuildRequest) -> Result { let conn = self.conn.lock().await; let guild_id: GuildId = req.external_id.0.into(); conn.execute( " INSERT INTO Guild (id, name, sound_delay) VALUES (?1, ?2, ?3) ", [ &guild_id.to_string(), &req.name, &req.sound_delay.to_string(), ], ) .context("failed to insert guild")?; Ok(Guild::new( guild_id, req.name, req.sound_delay, req.external_id, )) } async fn create_user(&self, req: CreateUserRequest) -> Result<(), CreateUserError> { let conn = self.conn.lock().await; conn.execute( " INSERT INTO User (username) VALUES (?1) ", [req.user.as_ref()], ) .context("failed to insert user")?; Ok(()) } async fn add_user_to_guild( &self, guild_id: GuildId, username: &str, ) -> Result<(), guild::AddUserToGuildError> { let conn = self.conn.lock().await; conn.execute( " INSERT OR IGNORE INTO UserGuild (username, guild_id) VALUES (?1, ?2) ", [username, &guild_id.to_string()], ) .context("failed to insert user guild")?; Ok(()) } async fn create_channel( &self, req: CreateChannelRequest, ) -> Result { todo!() } async fn add_intro_to_guild( &self, name: &str, guild_id: GuildId, filename: String, ) -> Result { let conn = self.conn.lock().await; let mut query = conn .prepare( " INSERT INTO Intro ( name, volume, guild_id, filename ) VALUES ( :name, :volume, :guild_id, :filename ) RETURNING id ", ) .context("failed to prepare query")?; let intro_id = query .query_row( &[ (":name", name), (":volume", &0.to_string()), (":guild_id", &guild_id.to_string()), (":filename", &filename), ], |row| Ok(row.get::<_, i32>(0)?.into()), ) .context("failed to query row")?; Ok(intro_id) } }