baseline stuff just for displaying intros
parent
eb23143739
commit
c4d12562a1
|
|
@ -0,0 +1,125 @@
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
|
||||||
|
use crate::lib::domain::intro_tool::{
|
||||||
|
models,
|
||||||
|
ports::{IntroToolRepository, IntroToolService},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DebugService<S>
|
||||||
|
where
|
||||||
|
S: IntroToolService,
|
||||||
|
{
|
||||||
|
impersonated_username: String,
|
||||||
|
wrapped_service: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> DebugService<S>
|
||||||
|
where
|
||||||
|
S: IntroToolService,
|
||||||
|
{
|
||||||
|
pub fn new(wrapped_service: S, impersonated_username: String) -> Self {
|
||||||
|
Self {
|
||||||
|
wrapped_service,
|
||||||
|
impersonated_username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> IntroToolService for DebugService<S>
|
||||||
|
where
|
||||||
|
S: IntroToolService,
|
||||||
|
{
|
||||||
|
async fn needs_setup(&self) -> bool {
|
||||||
|
self.wrapped_service.needs_setup().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild(
|
||||||
|
&self,
|
||||||
|
guild_id: impl Into<models::guild::GuildId> + Send,
|
||||||
|
) -> Result<models::guild::Guild, models::guild::GetGuildError> {
|
||||||
|
self.wrapped_service.get_guild(guild_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild_users(
|
||||||
|
&self,
|
||||||
|
guild_id: models::guild::GuildId,
|
||||||
|
) -> Result<Vec<models::guild::User>, models::guild::GetUserError> {
|
||||||
|
self.wrapped_service.get_guild_users(guild_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild_intros(
|
||||||
|
&self,
|
||||||
|
guild_id: models::guild::GuildId,
|
||||||
|
) -> Result<Vec<models::guild::Intro>, models::guild::GetIntroError> {
|
||||||
|
self.wrapped_service.get_guild_intros(guild_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> Result<models::guild::User, models::guild::GetUserError> {
|
||||||
|
self.wrapped_service.get_user(username).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_guilds(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> Result<Vec<models::guild::GuildRef>, models::guild::GetGuildError> {
|
||||||
|
self.wrapped_service.get_user_guilds(username).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_from_api_key(
|
||||||
|
&self,
|
||||||
|
_api_key: &str,
|
||||||
|
) -> Result<models::guild::User, models::guild::GetUserError> {
|
||||||
|
let user = self
|
||||||
|
.wrapped_service
|
||||||
|
.get_user(&self.impersonated_username)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(models::guild::User::new(
|
||||||
|
self.impersonated_username.clone(),
|
||||||
|
"testApiKey".into(),
|
||||||
|
Utc::now().naive_utc() + Duration::days(1),
|
||||||
|
"testDiscordToken".into(),
|
||||||
|
Utc::now().naive_utc() + Duration::days(1),
|
||||||
|
)
|
||||||
|
.with_channel_intros(user.intros().clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_guild(
|
||||||
|
&self,
|
||||||
|
req: models::guild::CreateGuildRequest,
|
||||||
|
) -> Result<models::guild::Guild, models::guild::CreateGuildError> {
|
||||||
|
self.wrapped_service.create_guild(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_user(
|
||||||
|
&self,
|
||||||
|
req: models::guild::CreateUserRequest,
|
||||||
|
) -> Result<models::guild::User, models::guild::CreateUserError> {
|
||||||
|
self.wrapped_service.create_user(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_channel(
|
||||||
|
&self,
|
||||||
|
req: models::guild::CreateChannelRequest,
|
||||||
|
) -> Result<models::guild::Channel, models::guild::CreateChannelError> {
|
||||||
|
self.wrapped_service.create_channel(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_intro_to_guild(
|
||||||
|
&self,
|
||||||
|
req: models::guild::AddIntroToGuildRequest,
|
||||||
|
) -> Result<(), models::guild::AddIntroToGuildError> {
|
||||||
|
self.wrapped_service.add_intro_to_guild(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_intro_to_user(
|
||||||
|
&self,
|
||||||
|
req: models::guild::AddIntroToUserRequest,
|
||||||
|
) -> Result<(), models::guild::AddIntroToUserError> {
|
||||||
|
self.wrapped_service.add_intro_to_user(req).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod debug_service;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod ports;
|
pub mod ports;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,226 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct GuildId(u64);
|
pub struct GuildId(u64);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct ExternalGuildId(u64);
|
pub struct ExternalGuildId(u64);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct UserName(String);
|
pub struct UserName(String);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct ChannelName(String);
|
pub struct ChannelName(String);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct IntroId(i32);
|
pub struct IntroId(i32);
|
||||||
|
|
||||||
pub struct Guild {
|
impl From<u64> for GuildId {
|
||||||
id: GuildId,
|
fn from(id: u64) -> Self {
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
name: String,
|
impl From<u64> for ExternalGuildId {
|
||||||
sound_delay: u32,
|
fn from(id: u64) -> Self {
|
||||||
external_id: ExternalGuildId,
|
Self(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i32> for IntroId {
|
||||||
|
fn from(id: i32) -> Self {
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for UserName {
|
||||||
|
fn from(name: String) -> Self {
|
||||||
|
Self(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for ChannelName {
|
||||||
|
fn from(name: String) -> Self {
|
||||||
|
Self(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for UserName {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for ChannelName {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for GuildId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for IntroId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Guild {
|
||||||
|
guild: GuildRef,
|
||||||
|
|
||||||
channels: Vec<Channel>,
|
channels: Vec<Channel>,
|
||||||
users: Vec<User>,
|
users: Vec<User>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct User {
|
#[derive(Debug)]
|
||||||
user: UserName,
|
pub struct GuildRef {
|
||||||
channel_intros: HashMap<ChannelName, Vec<Intro>>,
|
id: GuildId,
|
||||||
|
name: String,
|
||||||
|
sound_delay: u32,
|
||||||
|
external_id: ExternalGuildId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GuildRef {
|
||||||
|
pub fn id(&self) -> GuildId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GuildRef {
|
||||||
|
pub fn new(id: GuildId, name: String, sound_delay: u32, external_id: ExternalGuildId) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
sound_delay,
|
||||||
|
external_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Guild {
|
||||||
|
pub fn new(id: GuildId, name: String, sound_delay: u32, external_id: ExternalGuildId) -> Self {
|
||||||
|
Self {
|
||||||
|
guild: GuildRef {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
sound_delay,
|
||||||
|
external_id,
|
||||||
|
},
|
||||||
|
channels: vec![],
|
||||||
|
users: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> GuildId {
|
||||||
|
self.guild.id()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
self.guild.name()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn users(&self) -> &[User] {
|
||||||
|
&self.users
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn channels(&self) -> &[Channel] {
|
||||||
|
&self.channels
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_users(self, users: Vec<User>) -> Self {
|
||||||
|
Self { users, ..self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_channels(self, channels: Vec<Channel>) -> Self {
|
||||||
|
Self { channels, ..self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct User {
|
||||||
|
name: UserName,
|
||||||
|
|
||||||
|
api_key: String,
|
||||||
|
api_key_expires_at: NaiveDateTime,
|
||||||
|
discord_token: String,
|
||||||
|
discord_token_expires_at: NaiveDateTime,
|
||||||
|
|
||||||
|
channel_intros: HashMap<(GuildId, ChannelName), Vec<Intro>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub fn new(
|
||||||
|
name: impl Into<UserName>,
|
||||||
|
api_key: String,
|
||||||
|
api_key_expires_at: NaiveDateTime,
|
||||||
|
discord_token: String,
|
||||||
|
discord_token_expires_at: NaiveDateTime,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
api_key,
|
||||||
|
api_key_expires_at,
|
||||||
|
discord_token,
|
||||||
|
discord_token_expires_at,
|
||||||
|
channel_intros: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn intros(&self) -> &HashMap<(GuildId, ChannelName), Vec<Intro>> {
|
||||||
|
&self.channel_intros
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn api_key_expires_at(&self) -> NaiveDateTime {
|
||||||
|
self.api_key_expires_at
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn discord_token_expires_at(&self) -> NaiveDateTime {
|
||||||
|
self.discord_token_expires_at
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_channel_intros(
|
||||||
|
self,
|
||||||
|
channel_intros: HashMap<(GuildId, ChannelName), Vec<Intro>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
channel_intros,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
name: ChannelName,
|
name: ChannelName,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Channel {
|
||||||
|
pub fn new(name: ChannelName) -> Self {
|
||||||
|
Self { name }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &ChannelName {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Intro {
|
pub struct Intro {
|
||||||
id: IntroId,
|
id: IntroId,
|
||||||
|
|
||||||
|
|
@ -35,6 +228,20 @@ pub struct Intro {
|
||||||
filename: String,
|
filename: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Intro {
|
||||||
|
pub fn new(id: IntroId, name: String, filename: String) -> Self {
|
||||||
|
Self { id, name, filename }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> IntroId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct CreateGuildRequest {
|
pub struct CreateGuildRequest {
|
||||||
name: String,
|
name: String,
|
||||||
sound_delay: u32,
|
sound_delay: u32,
|
||||||
|
|
@ -99,6 +306,45 @@ pub enum GetGuildError {
|
||||||
#[error("Guild not found")]
|
#[error("Guild not found")]
|
||||||
NotFound,
|
NotFound,
|
||||||
|
|
||||||
|
#[error("Could not fetch guild users")]
|
||||||
|
CouldNotFetchUsers(#[from] GetUserError),
|
||||||
|
|
||||||
|
#[error("Could not fetch guild channels")]
|
||||||
|
CouldNotFetchChannels(#[from] GetChannelError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GetUserError {
|
||||||
|
#[error("User not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error("Could not fetch user guilds")]
|
||||||
|
CouldNotFetchGuilds(#[from] Box<GetGuildError>),
|
||||||
|
|
||||||
|
#[error("Could not fetch user channel intros")]
|
||||||
|
CouldNotFetchChannelIntros(#[from] GetIntroError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GetChannelError {
|
||||||
|
#[error("Channel not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GetIntroError {
|
||||||
|
#[error("Intro not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Unknown(#[from] anyhow::Error),
|
Unknown(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,101 @@
|
||||||
|
use std::{collections::HashMap, future::Future};
|
||||||
|
|
||||||
|
use crate::lib::domain::intro_tool::models::guild::ChannelName;
|
||||||
|
|
||||||
use super::models::guild::{
|
use super::models::guild::{
|
||||||
AddIntroToGuildError, AddIntroToGuildRequest, AddIntroToUserError, AddIntroToUserRequest,
|
AddIntroToGuildError, AddIntroToGuildRequest, AddIntroToUserError, AddIntroToUserRequest,
|
||||||
Channel, CreateChannelError, CreateChannelRequest, CreateGuildError, CreateGuildRequest,
|
Channel, CreateChannelError, CreateChannelRequest, CreateGuildError, CreateGuildRequest,
|
||||||
CreateUserError, CreateUserRequest, GetGuildError, Guild, GuildId, User,
|
CreateUserError, CreateUserRequest, GetChannelError, GetGuildError, GetIntroError,
|
||||||
|
GetUserError, Guild, GuildId, GuildRef, Intro, User,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait IntroToolService {
|
pub trait IntroToolService: Send + Sync + Clone + 'static {
|
||||||
async fn create_guild(&self, req: CreateGuildRequest) -> Result<Guild, CreateGuildError>;
|
fn needs_setup(&self) -> impl Future<Output = bool> + Send;
|
||||||
async fn create_user(&self, req: CreateUserRequest) -> Result<User, CreateUserError>;
|
|
||||||
async fn create_channel(
|
|
||||||
&self,
|
|
||||||
req: CreateChannelRequest,
|
|
||||||
) -> Result<Channel, CreateChannelError>;
|
|
||||||
|
|
||||||
async fn add_intro_to_guild(
|
fn get_guild(
|
||||||
&self,
|
&self,
|
||||||
req: AddIntroToGuildRequest,
|
guild_id: impl Into<GuildId> + Send,
|
||||||
) -> Result<(), AddIntroToGuildError>;
|
) -> impl Future<Output = Result<Guild, GetGuildError>> + Send;
|
||||||
|
fn get_guild_users(
|
||||||
async fn add_intro_to_user(
|
|
||||||
&self,
|
&self,
|
||||||
req: AddIntroToUserRequest,
|
guild_id: GuildId,
|
||||||
) -> Result<(), AddIntroToUserError>;
|
) -> impl Future<Output = Result<Vec<User>, GetUserError>> + Send;
|
||||||
}
|
fn get_guild_intros(
|
||||||
|
&self,
|
||||||
pub trait IntroToolRepository {
|
guild_id: GuildId,
|
||||||
async fn get_guild(&self, guild_id: GuildId) -> Result<Guild, GetGuildError>;
|
) -> impl Future<Output = Result<Vec<Intro>, GetIntroError>> + Send;
|
||||||
|
fn get_user(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> impl Future<Output = Result<User, GetUserError>> + Send;
|
||||||
|
fn get_user_guilds(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> impl Future<Output = Result<Vec<GuildRef>, GetGuildError>> + Send;
|
||||||
|
fn get_user_from_api_key(
|
||||||
|
&self,
|
||||||
|
api_key: &str,
|
||||||
|
) -> impl Future<Output = Result<User, GetUserError>> + Send;
|
||||||
|
|
||||||
|
async fn create_guild(&self, req: CreateGuildRequest) -> Result<Guild, CreateGuildError>;
|
||||||
|
async fn create_user(&self, req: CreateUserRequest) -> Result<User, CreateUserError>;
|
||||||
|
async fn create_channel(
|
||||||
|
&self,
|
||||||
|
req: CreateChannelRequest,
|
||||||
|
) -> Result<Channel, CreateChannelError>;
|
||||||
|
|
||||||
|
async fn add_intro_to_guild(
|
||||||
|
&self,
|
||||||
|
req: AddIntroToGuildRequest,
|
||||||
|
) -> Result<(), AddIntroToGuildError>;
|
||||||
|
|
||||||
|
async fn add_intro_to_user(
|
||||||
|
&self,
|
||||||
|
req: AddIntroToUserRequest,
|
||||||
|
) -> Result<(), AddIntroToUserError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IntroToolRepository: Send + Sync + Clone + 'static {
|
||||||
|
fn get_guild(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> impl Future<Output = Result<Guild, GetGuildError>> + Send;
|
||||||
|
fn get_guild_count(&self) -> impl Future<Output = Result<usize, GetGuildError>> + Send;
|
||||||
|
|
||||||
|
fn get_guild_users(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> impl Future<Output = Result<Vec<User>, GetUserError>> + Send;
|
||||||
|
|
||||||
|
fn get_guild_channels(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> impl Future<Output = Result<Vec<Channel>, GetChannelError>> + Send;
|
||||||
|
fn get_guild_intros(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> impl Future<Output = Result<Vec<Intro>, GetIntroError>> + Send;
|
||||||
|
|
||||||
|
fn get_user(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> impl Future<Output = Result<User, GetUserError>> + Send;
|
||||||
|
|
||||||
|
fn get_user_channel_intros(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> impl Future<Output = Result<HashMap<(GuildId, ChannelName), Vec<Intro>>, GetIntroError>> + Send;
|
||||||
|
|
||||||
|
fn get_user_guilds(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> impl Future<Output = Result<Vec<GuildRef>, GetGuildError>> + Send;
|
||||||
|
|
||||||
|
fn get_user_from_api_key(
|
||||||
|
&self,
|
||||||
|
api_key: &str,
|
||||||
|
) -> impl Future<Output = Result<User, GetUserError>> + Send;
|
||||||
|
|
||||||
async fn create_guild(&self, req: CreateGuildRequest) -> Result<Guild, CreateGuildError>;
|
async fn create_guild(&self, req: CreateGuildRequest) -> Result<Guild, CreateGuildError>;
|
||||||
async fn create_user(&self, req: CreateUserRequest) -> Result<User, CreateUserError>;
|
async fn create_user(&self, req: CreateUserRequest) -> Result<User, CreateUserError>;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
use crate::lib::domain::intro_tool::ports::{IntroToolRepository, IntroToolService};
|
use crate::lib::domain::intro_tool::{
|
||||||
|
models::guild::{GetUserError, GuildId, User},
|
||||||
|
ports::{IntroToolRepository, IntroToolService},
|
||||||
|
};
|
||||||
|
|
||||||
use super::models;
|
use super::models;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Service<R>
|
pub struct Service<R>
|
||||||
where
|
where
|
||||||
R: IntroToolRepository,
|
R: IntroToolRepository,
|
||||||
|
|
@ -22,6 +26,49 @@ impl<R> IntroToolService for Service<R>
|
||||||
where
|
where
|
||||||
R: IntroToolRepository,
|
R: IntroToolRepository,
|
||||||
{
|
{
|
||||||
|
async fn needs_setup(&self) -> bool {
|
||||||
|
let Ok(guild_count) = self.repo.get_guild_count().await else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
guild_count == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild(
|
||||||
|
&self,
|
||||||
|
guild_id: impl Into<GuildId>,
|
||||||
|
) -> Result<models::guild::Guild, models::guild::GetGuildError> {
|
||||||
|
self.repo.get_guild(guild_id.into()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild_users(&self, guild_id: GuildId) -> Result<Vec<User>, GetUserError> {
|
||||||
|
self.repo.get_guild_users(guild_id).await
|
||||||
|
}
|
||||||
|
async fn get_guild_intros(
|
||||||
|
&self,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> Result<Vec<models::guild::Intro>, models::guild::GetIntroError> {
|
||||||
|
self.repo.get_guild_intros(guild_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> Result<models::guild::User, models::guild::GetUserError> {
|
||||||
|
self.repo.get_user(username).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_guilds(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str> + Send,
|
||||||
|
) -> Result<Vec<models::guild::GuildRef>, models::guild::GetGuildError> {
|
||||||
|
self.repo.get_user_guilds(username).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_from_api_key(&self, api_key: &str) -> Result<User, GetUserError> {
|
||||||
|
self.repo.get_user_from_api_key(api_key).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_guild(
|
async fn create_guild(
|
||||||
&self,
|
&self,
|
||||||
req: models::guild::CreateGuildRequest,
|
req: models::guild::CreateGuildRequest,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod http;
|
||||||
|
pub mod response;
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
mod page;
|
||||||
|
|
||||||
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::FromRequestParts,
|
||||||
|
http::request::Parts,
|
||||||
|
response::Redirect,
|
||||||
|
routing::{get, post},
|
||||||
|
};
|
||||||
|
use axum_extra::extract::CookieJar;
|
||||||
|
use chrono::Utc;
|
||||||
|
use reqwest::Method;
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
auth,
|
||||||
|
lib::domain::intro_tool::{models::guild::User, ports::IntroToolService},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct ApiState<S>
|
||||||
|
where
|
||||||
|
S: IntroToolService,
|
||||||
|
{
|
||||||
|
intro_tool_service: Arc<S>,
|
||||||
|
|
||||||
|
pub secrets: auth::DiscordSecret,
|
||||||
|
pub origin: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[axum::async_trait]
|
||||||
|
impl<S: IntroToolService> FromRequestParts<ApiState<S>> for User {
|
||||||
|
type Rejection = Redirect;
|
||||||
|
|
||||||
|
async fn from_request_parts(
|
||||||
|
Parts { headers, .. }: &mut Parts,
|
||||||
|
state: &ApiState<S>,
|
||||||
|
) -> Result<Self, Self::Rejection> {
|
||||||
|
let jar = CookieJar::from_headers(headers);
|
||||||
|
|
||||||
|
if let Some(token) = jar.get("access_token") {
|
||||||
|
match state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_user_from_api_key(token.value())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(user) => {
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
if user.api_key_expires_at() < now || user.discord_token_expires_at() < now {
|
||||||
|
Err(Redirect::to(&format!("{}/login", state.origin)))
|
||||||
|
} else {
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(?err, "failed to authenticate user");
|
||||||
|
|
||||||
|
Err(Redirect::to(&format!("{}/login", state.origin)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(Redirect::to(&format!("{}/login", state.origin)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HttpServer {
|
||||||
|
make_service: axum::routing::IntoMakeService<axum::Router>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpServer {
|
||||||
|
pub fn new(
|
||||||
|
intro_tool_service: impl IntroToolService,
|
||||||
|
secrets: auth::DiscordSecret,
|
||||||
|
origin: String,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let state = ApiState {
|
||||||
|
intro_tool_service: Arc::new(intro_tool_service),
|
||||||
|
secrets,
|
||||||
|
origin: origin.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let router = routes()
|
||||||
|
.layer(
|
||||||
|
CorsLayer::new()
|
||||||
|
.allow_origin([origin.parse().unwrap()])
|
||||||
|
.allow_headers(tower_http::cors::Any)
|
||||||
|
.allow_methods([Method::GET, Method::POST, Method::DELETE]),
|
||||||
|
)
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
make_service: router.into_make_service(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(self) {
|
||||||
|
let addr = SocketAddr::from(([0, 0, 0, 0], 8100));
|
||||||
|
info!("socket listening on {addr}");
|
||||||
|
|
||||||
|
axum::Server::bind(&addr)
|
||||||
|
.serve(self.make_service)
|
||||||
|
.await
|
||||||
|
.expect("couldn't start http server");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn routes<S>() -> axum::Router<ApiState<S>>
|
||||||
|
where
|
||||||
|
S: IntroToolService,
|
||||||
|
{
|
||||||
|
axum::Router::<ApiState<S>>::new()
|
||||||
|
.route("/", get(page::home))
|
||||||
|
.route("/login", get(page::login))
|
||||||
|
.route("/guild/:guild_id", get(page::guild_dashboard))
|
||||||
|
// .route("/", get(page::home))
|
||||||
|
// .route("/index.html", get(page::home))
|
||||||
|
// .route("/login", get(page::login))
|
||||||
|
// .route("/guild/:guild_id", get(page::guild_dashboard))
|
||||||
|
// .route("/guild/:guild_id/setup", get(routes::guild_setup))
|
||||||
|
// .route(
|
||||||
|
// "/guild/:guild_id/add_channel",
|
||||||
|
// post(routes::guild_add_channel),
|
||||||
|
// )
|
||||||
|
// .route(
|
||||||
|
// "/guild/:guild_id/permissions/update",
|
||||||
|
// post(routes::update_guild_permissions),
|
||||||
|
// )
|
||||||
|
// .route("/v2/auth", get(routes::v2_auth))
|
||||||
|
// .route(
|
||||||
|
// "/v2/intros/add/:guild_id/:channel",
|
||||||
|
// post(routes::v2_add_intro_to_user),
|
||||||
|
// )
|
||||||
|
// .route(
|
||||||
|
// "/v2/intros/remove/:guild_id/:channel",
|
||||||
|
// post(routes::v2_remove_intro_from_user),
|
||||||
|
// )
|
||||||
|
// .route("/v2/intros/:guild/add", get(routes::v2_add_guild_intro))
|
||||||
|
// .route(
|
||||||
|
// "/v2/intros/:guild/upload",
|
||||||
|
// post(routes::v2_upload_guild_intro),
|
||||||
|
// )
|
||||||
|
// .route("/health", get(routes::health))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,292 @@
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
response::{Html, Redirect},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
htmx::{Build, HtmxBuilder, Tag},
|
||||||
|
lib::{
|
||||||
|
domain::intro_tool::{
|
||||||
|
models::guild::{ChannelName, GuildRef, Intro, User},
|
||||||
|
ports::IntroToolService,
|
||||||
|
},
|
||||||
|
inbound::{http::ApiState, response::ErrorAsRedirect},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn home<S: IntroToolService>(
|
||||||
|
State(state): State<ApiState<S>>,
|
||||||
|
user: Option<User>,
|
||||||
|
) -> Result<impl axum::response::IntoResponse, Redirect> {
|
||||||
|
if let Some(user) = user {
|
||||||
|
let needs_setup = state.intro_tool_service.needs_setup().await;
|
||||||
|
let user_guilds = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_user_guilds(user.name())
|
||||||
|
.await
|
||||||
|
.as_redirect(&state.origin, "/login")?;
|
||||||
|
|
||||||
|
// TODO: get user app permissions
|
||||||
|
// TODO: check if user can add guilds
|
||||||
|
// TODO: fetch guilds from discord
|
||||||
|
|
||||||
|
let can_add_guild = false;
|
||||||
|
let discord_guilds: Vec<GuildRef> = vec![];
|
||||||
|
|
||||||
|
let guild_list = if needs_setup {
|
||||||
|
// TODO:
|
||||||
|
// HtmxBuilder::new(Tag::Empty).builder(Tag::Div, |b| {
|
||||||
|
// b.attribute("class", "container")
|
||||||
|
// .builder_text(Tag::Header2, "Select a Guild to setup")
|
||||||
|
// .push_builder(setup_guild_list(&state.origin, &discord_guilds))
|
||||||
|
// })
|
||||||
|
todo!()
|
||||||
|
} else {
|
||||||
|
HtmxBuilder::new(Tag::Empty).builder(Tag::Div, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.builder_text(Tag::Header2, "Choose a Guild")
|
||||||
|
.push_builder(guild_list(&state.origin, user_guilds.iter()))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Html(
|
||||||
|
page_header("MemeJoin - Home")
|
||||||
|
.builder(Tag::Div, |b| {
|
||||||
|
b.push_builder(guild_list)
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// let mut b = b.push_builder(guild_list);
|
||||||
|
//
|
||||||
|
// if !needs_setup && can_add_guild && !discord_guilds.is_empty() {
|
||||||
|
// b = b
|
||||||
|
// .attribute("class", "container")
|
||||||
|
// .builder_text(Tag::Header2, "Add a Guild")
|
||||||
|
// .push_builder(setup_guild_list(&state.origin, &discord_guilds));
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// b
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(Redirect::to(&format!("{}/login", state.origin)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login<S: IntroToolService>(
|
||||||
|
State(state): State<ApiState<S>>,
|
||||||
|
user: Option<User>,
|
||||||
|
) -> Result<Html<String>, Redirect> {
|
||||||
|
if user.is_some() {
|
||||||
|
Err(Redirect::to(&format!("{}/", state.origin)))
|
||||||
|
} else {
|
||||||
|
let authorize_uri = format!("https://discord.com/api/oauth2/authorize?client_id={}&redirect_uri={}/v2/auth&response_type=code&scope=guilds.members.read+guilds+identify", state.secrets.client_id, state.origin);
|
||||||
|
|
||||||
|
Ok(Html(
|
||||||
|
HtmxBuilder::new(Tag::Html)
|
||||||
|
.push_builder(page_header("MemeJoin - Dashboard"))
|
||||||
|
.builder(Tag::Nav, |b| {
|
||||||
|
b.builder(Tag::HeaderGroup, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.builder(Tag::Header1, |b| b.text("MemeJoin - A bot for user intros"))
|
||||||
|
.builder_text(Tag::Header6, "salad")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.builder(Tag::Main, |b| {
|
||||||
|
b.attribute("class", "container").builder(Tag::Anchor, |b| {
|
||||||
|
b.attribute("role", "button")
|
||||||
|
.text("Login with Discord")
|
||||||
|
.attribute("href", &authorize_uri)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn guild_dashboard<S: IntroToolService>(
|
||||||
|
State(state): State<ApiState<S>>,
|
||||||
|
user: User,
|
||||||
|
Path(guild_id): Path<u64>,
|
||||||
|
) -> Result<Html<String>, Redirect> {
|
||||||
|
let guild = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_guild(guild_id)
|
||||||
|
.await
|
||||||
|
.as_redirect(&state.origin, "/login")?;
|
||||||
|
let user_guilds = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_user_guilds(user.name())
|
||||||
|
.await
|
||||||
|
.as_redirect(&state.origin, "/login")?;
|
||||||
|
let guild_intros = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_guild_intros(guild_id.into())
|
||||||
|
.await
|
||||||
|
.as_redirect(&state.origin, "/login")?;
|
||||||
|
|
||||||
|
// does user have access to this guild
|
||||||
|
if !user_guilds
|
||||||
|
.iter()
|
||||||
|
.any(|guild_ref| guild_ref.id() == guild.id())
|
||||||
|
{
|
||||||
|
return Err(Redirect::to(&format!("{}/error", state.origin)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Html(
|
||||||
|
HtmxBuilder::new(Tag::Html)
|
||||||
|
.push_builder(page_header("MemeJoin - Dashboard"))
|
||||||
|
.builder(Tag::Nav, |b| {
|
||||||
|
b.builder(Tag::HeaderGroup, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.builder(Tag::Header1, |b| b.text("MemeJoin - A bot for user intros"))
|
||||||
|
.builder_text(Tag::Header6, &format!("{} - {}", user.name(), guild.name()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.builder(Tag::Empty, |b| {
|
||||||
|
// TODO:
|
||||||
|
// let mut b = if is_moderator || can_add_channel {
|
||||||
|
// b.builder(Tag::Div, |b| {
|
||||||
|
// b.attribute("class", "container")
|
||||||
|
// .builder(Tag::Article, |b| {
|
||||||
|
// b.builder_text(Tag::Header, "Server Settings")
|
||||||
|
// .push_builder(mod_dashboard)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// b
|
||||||
|
// };
|
||||||
|
// b = if can_upload {
|
||||||
|
// b.builder(Tag::Div, |b| {
|
||||||
|
// b.attribute("class", "container")
|
||||||
|
// .builder(Tag::Article, |b| {
|
||||||
|
// b.builder_text(Tag::Header, "Upload New Intro")
|
||||||
|
// .push_builder(upload_form(&state.origin, guild_id))
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// .builder(Tag::Div, |b| {
|
||||||
|
// b.attribute("class", "container")
|
||||||
|
// .builder(Tag::Article, |b| {
|
||||||
|
// b.builder_text(Tag::Header, "Upload New Intro from Url")
|
||||||
|
// .push_builder(ytdl_form(&state.origin, guild_id))
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// b
|
||||||
|
// };
|
||||||
|
|
||||||
|
b.builder(Tag::Div, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.builder(Tag::Article, |b| {
|
||||||
|
let mut b = b.builder_text(Tag::Header, "Guild Intros");
|
||||||
|
|
||||||
|
for guild_channel in guild.channels() {
|
||||||
|
let intros = user.intros().get(&(guild.id(), guild_channel.name().clone())).map(|intros| intros.iter()).unwrap_or_default();
|
||||||
|
|
||||||
|
b = b.builder(Tag::Details, |b| {
|
||||||
|
let mut b = b;
|
||||||
|
if guild.channels().len() < 2 {
|
||||||
|
b = b.attribute("open", "");
|
||||||
|
}
|
||||||
|
b.builder_text(Tag::Summary, guild_channel.name().as_ref()).builder(
|
||||||
|
Tag::Div,
|
||||||
|
|b| {
|
||||||
|
b.attribute("id", "channel-intro-selector")
|
||||||
|
.attribute("style", "display: flex; align-items: flex-end; max-height: 50%; overflow: hidden;")
|
||||||
|
.push_builder(channel_intro_selector(
|
||||||
|
&state.origin,
|
||||||
|
guild_id,
|
||||||
|
guild_channel.name(),
|
||||||
|
intros,
|
||||||
|
guild_intros.iter(),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
b
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn page_header(title: &str) -> HtmxBuilder {
|
||||||
|
HtmxBuilder::new(Tag::Html).head(|b| {
|
||||||
|
b.title(title)
|
||||||
|
.script(
|
||||||
|
"https://unpkg.com/htmx.org@1.9.3",
|
||||||
|
Some("sha384-lVb3Rd/Ca0AxaoZg5sACe8FJKF0tnUgR2Kd7ehUOG5GCcROv5uBIZsOqovBAcWua"),
|
||||||
|
)
|
||||||
|
// Not currently using
|
||||||
|
// .script("https://unpkg.com/hyperscript.org@0.9.9", None)
|
||||||
|
.style_link("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guild_list<'a>(origin: &str, guilds: impl Iterator<Item = &'a GuildRef>) -> HtmxBuilder {
|
||||||
|
HtmxBuilder::new(Tag::Empty).ul(|b| {
|
||||||
|
let mut b = b;
|
||||||
|
for guild in guilds {
|
||||||
|
b = b.li(|b| b.link(guild.name(), &format!("{}/guild/{}", origin, guild.id())));
|
||||||
|
}
|
||||||
|
|
||||||
|
b
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn channel_intro_selector<'a>(
|
||||||
|
origin: &str,
|
||||||
|
guild_id: u64,
|
||||||
|
channel_name: &ChannelName,
|
||||||
|
intros: impl Iterator<Item = &'a Intro>,
|
||||||
|
guild_intros: impl Iterator<Item = &'a Intro>,
|
||||||
|
) -> HtmxBuilder {
|
||||||
|
HtmxBuilder::new(Tag::Empty)
|
||||||
|
.builder(Tag::Div, |b| {
|
||||||
|
b.attribute("style", "display: flex; flex-direction: column; justify-content: space-between; align-items: center; width: 100%; height: 100%; padding: 16px;")
|
||||||
|
.builder_text(Tag::Strong, "Your Current Intros")
|
||||||
|
.push_builder(intro_list(
|
||||||
|
intros,
|
||||||
|
"Remove Intro",
|
||||||
|
&format!("{}/v2/intros/remove/{}/{}", origin, guild_id, channel_name.as_ref()),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.builder(Tag::Div, |b| {
|
||||||
|
b.attribute("style", "display: flex; flex-direction: column; justify-content: space-between; align-items: center; width: 100%; height: 100%; padding: 16px;")
|
||||||
|
.builder_text(Tag::Strong, "Select Intros")
|
||||||
|
.push_builder(intro_list(
|
||||||
|
guild_intros,
|
||||||
|
"Add Intro",
|
||||||
|
&format!("{}/v2/intros/add/{}/{}", origin, guild_id, channel_name.as_ref()),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn intro_list<'a>(intros: impl Iterator<Item = &'a Intro>, label: &str, post: &str) -> HtmxBuilder {
|
||||||
|
HtmxBuilder::new(Tag::Empty).form(|b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.hx_post(post)
|
||||||
|
.hx_target("closest #channel-intro-selector")
|
||||||
|
.attribute("hx-encoding", "multipart/form-data")
|
||||||
|
.builder(Tag::FieldSet, |b| {
|
||||||
|
let mut b = b
|
||||||
|
.attribute("class", "container")
|
||||||
|
.attribute("style", "height: 256px; overflow: auto");
|
||||||
|
for intro in intros {
|
||||||
|
b = b.builder(Tag::Label, |b| {
|
||||||
|
b.builder(Tag::Input, |b| {
|
||||||
|
b.attribute("type", "checkbox")
|
||||||
|
.attribute("name", &intro.id().to_string())
|
||||||
|
})
|
||||||
|
.builder_text(Tag::Paragraph, intro.name())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
b
|
||||||
|
})
|
||||||
|
.button(|b| b.attribute("type", "submit").text(label))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use axum::response::Redirect;
|
||||||
|
|
||||||
|
use crate::lib::domain::intro_tool::models::guild::{
|
||||||
|
GetChannelError, GetGuildError, GetIntroError,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(super) trait ErrorAsRedirect<T>: Sized {
|
||||||
|
fn as_redirect(self, origin: impl AsRef<str>, path: impl AsRef<str>) -> Result<T, Redirect>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Debug> ErrorAsRedirect<T> for Result<T, GetGuildError> {
|
||||||
|
fn as_redirect(self, origin: impl AsRef<str>, path: impl AsRef<str>) -> Result<T, Redirect> {
|
||||||
|
match self {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(GetGuildError::NotFound)
|
||||||
|
| Err(GetGuildError::CouldNotFetchUsers(_))
|
||||||
|
| Err(GetGuildError::CouldNotFetchChannels(_))
|
||||||
|
| Err(GetGuildError::Unknown(_)) => {
|
||||||
|
tracing::error!(err = ?self, "failed to get guild");
|
||||||
|
|
||||||
|
Err(Redirect::to(&format!(
|
||||||
|
"{}/{}",
|
||||||
|
origin.as_ref(),
|
||||||
|
path.as_ref()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Debug> ErrorAsRedirect<T> for Result<T, GetChannelError> {
|
||||||
|
fn as_redirect(self, origin: impl AsRef<str>, path: impl AsRef<str>) -> Result<T, Redirect> {
|
||||||
|
match self {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(GetChannelError::NotFound) | Err(GetChannelError::Unknown(_)) => {
|
||||||
|
tracing::error!(err = ?self, "failed to get channel");
|
||||||
|
|
||||||
|
Err(Redirect::to(&format!(
|
||||||
|
"{}/{}",
|
||||||
|
origin.as_ref(),
|
||||||
|
path.as_ref()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Debug> ErrorAsRedirect<T> for Result<T, GetIntroError> {
|
||||||
|
fn as_redirect(self, origin: impl AsRef<str>, path: impl AsRef<str>) -> Result<T, Redirect> {
|
||||||
|
match self {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(GetIntroError::NotFound) | Err(GetIntroError::Unknown(_)) => {
|
||||||
|
tracing::error!(err = ?self, "failed to get intro");
|
||||||
|
|
||||||
|
Err(Redirect::to(&format!(
|
||||||
|
"{}/{}",
|
||||||
|
origin.as_ref(),
|
||||||
|
path.as_ref()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,333 @@
|
||||||
|
use iter_tools::Itertools;
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use rusqlite::Connection;
|
||||||
|
|
||||||
use crate::lib::domain::intro_tool::{
|
use crate::lib::domain::intro_tool::{
|
||||||
models::guild::{
|
models::guild::{
|
||||||
self, AddIntroToGuildError, AddIntroToGuildRequest, AddIntroToUserRequest, Channel,
|
self, AddIntroToGuildError, AddIntroToGuildRequest, AddIntroToUserRequest, Channel,
|
||||||
CreateChannelError, CreateChannelRequest, CreateGuildError, CreateGuildRequest,
|
ChannelName, CreateChannelError, CreateChannelRequest, CreateGuildError,
|
||||||
CreateUserError, CreateUserRequest, GetGuildError, Guild, GuildId, User,
|
CreateGuildRequest, CreateUserError, CreateUserRequest, GetChannelError, GetGuildError,
|
||||||
|
GetIntroError, GetUserError, Guild, GuildId, GuildRef, Intro, User, UserName,
|
||||||
},
|
},
|
||||||
ports::IntroToolRepository,
|
ports::IntroToolRepository,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Sqlite {}
|
#[derive(Clone)]
|
||||||
|
pub struct Sqlite {
|
||||||
|
conn: Arc<Mutex<Connection>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Sqlite {
|
impl Sqlite {
|
||||||
pub fn new(path: &str) -> Result<Self, std::io::Error> {
|
pub fn new(path: &str) -> rusqlite::Result<Self> {
|
||||||
todo!()
|
Ok(Self {
|
||||||
|
conn: Arc::new(Mutex::new(Connection::open(path)?)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntroToolRepository for Sqlite {
|
impl IntroToolRepository for Sqlite {
|
||||||
async fn get_guild(&self, guild_id: GuildId) -> Result<Guild, GetGuildError> {
|
async fn get_guild(&self, guild_id: GuildId) -> Result<Guild, GetGuildError> {
|
||||||
todo!()
|
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_guild_count(&self) -> Result<usize, GetGuildError> {
|
||||||
|
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<Vec<User>, 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::<Result<_, _>>()
|
||||||
|
.context("failed to fetch guild user rows")?;
|
||||||
|
|
||||||
|
Ok(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_guilds(
|
||||||
|
&self,
|
||||||
|
username: impl AsRef<str>,
|
||||||
|
) -> Result<Vec<GuildRef>, 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::<Result<_, _>>()
|
||||||
|
.context("failed to fetch guild user rows")?;
|
||||||
|
|
||||||
|
Ok(guilds)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild_channels(&self, guild_id: GuildId) -> Result<Vec<Channel>, 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::<Result<_, _>>()
|
||||||
|
.context("failed to fetch guild channel rows")?;
|
||||||
|
|
||||||
|
Ok(channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_guild_intros(&self, guild_id: GuildId) -> Result<Vec<Intro>, 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::<Result<_, _>>()
|
||||||
|
.context("failed to fetch guild intro rows")?;
|
||||||
|
|
||||||
|
Ok(intros)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user(&self, username: impl AsRef<str>) -> Result<User, GetUserError> {
|
||||||
|
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<str>,
|
||||||
|
guild_id: GuildId,
|
||||||
|
) -> Result<HashMap<(GuildId, ChannelName), Vec<Intro>>, 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::<Result<Vec<ChannelIntro>, _>>()
|
||||||
|
.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_from_api_key(&self, api_key: &str) -> Result<User, GetUserError> {
|
||||||
|
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 create_guild(&self, req: CreateGuildRequest) -> Result<Guild, CreateGuildError> {
|
async fn create_guild(&self, req: CreateGuildRequest) -> Result<Guild, CreateGuildError> {
|
||||||
|
|
|
||||||
29
src/main.rs
29
src/main.rs
|
|
@ -31,7 +31,7 @@ use songbird::SerenityInit;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
use crate::lib::domain::intro_tool;
|
use crate::lib::domain::intro_tool;
|
||||||
use crate::lib::outbound;
|
use crate::lib::{inbound, outbound};
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
|
|
||||||
enum HandlerMessage {
|
enum HandlerMessage {
|
||||||
|
|
@ -322,11 +322,32 @@ async fn main() -> std::io::Result<()> {
|
||||||
&std::fs::read_to_string("config/settings.json").expect("no config/settings.json"),
|
&std::fs::read_to_string("config/settings.json").expect("no config/settings.json"),
|
||||||
)
|
)
|
||||||
.expect("error parsing settings file");
|
.expect("error parsing settings file");
|
||||||
|
let secrets = auth::DiscordSecret {
|
||||||
|
client_id: env::var("DISCORD_CLIENT_ID").expect("expected DISCORD_CLIENT_ID env var"),
|
||||||
|
client_secret: env::var("DISCORD_CLIENT_SECRET")
|
||||||
|
.expect("expected DISCORD_CLIENT_SECRET env var"),
|
||||||
|
bot_token: env::var("DISCORD_TOKEN").expect("expected DISCORD_TOKEN env var"),
|
||||||
|
};
|
||||||
|
let origin = env::var("APP_ORIGIN").expect("expected APP_ORIGIN");
|
||||||
|
|
||||||
let db = outbound::sqlite::Sqlite::new(".config/db.sqlite").expect("couldn't open sqlite db");
|
let db = outbound::sqlite::Sqlite::new("./config/db.sqlite").expect("couldn't open sqlite db");
|
||||||
let service = intro_tool::service::Service::new(db);
|
|
||||||
|
|
||||||
// TODO: http server
|
if let Ok(impersonated_username) = env::var("IMPERSONATED_USERNAME") {
|
||||||
|
let service = intro_tool::service::Service::new(db);
|
||||||
|
let service = intro_tool::debug_service::DebugService::new(service, impersonated_username);
|
||||||
|
|
||||||
|
let http_server = inbound::http::HttpServer::new(service, secrets, origin)
|
||||||
|
.expect("couldn't start http server");
|
||||||
|
|
||||||
|
http_server.run().await;
|
||||||
|
} else {
|
||||||
|
let service = intro_tool::service::Service::new(db);
|
||||||
|
|
||||||
|
let http_server = inbound::http::HttpServer::new(service, secrets, origin)
|
||||||
|
.expect("couldn't start http server");
|
||||||
|
|
||||||
|
http_server.run().await;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue