diff --git a/src/application.rs b/src/application.rs index 474322c..51d3cec 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,4 +1,46 @@ +use serde::Serialize; +use sqlx::Error; + +use crate::persistence::{team::TeamRepository, user::UserRepository}; + pub mod authentication; pub mod calendar; pub mod talks; pub mod teams; + +#[derive(Clone, Debug, Serialize)] +pub struct User { + pub id: i64, + pub name: String, + pub team: String, +} + +async fn user_ids_to_users( + user_ids: Vec, + user_repository: &impl UserRepository, + team_repository: &impl TeamRepository, +) -> Result, Error> { + let mut users = Vec::new(); + for user_id in user_ids { + users.push(user_id_to_user(user_id, user_repository, team_repository).await?); + } + Ok(users) +} + +async fn user_id_to_user( + user_id: i64, + user_repository: &impl UserRepository, + team_repository: &impl TeamRepository, +) -> Result { + let Some((name, team_id)) = user_repository.get_name_and_team_id_by_id(user_id).await? else { + return Err(Error::RowNotFound); + }; + let Some(team) = team_repository.get_name_by_id(team_id).await? else { + return Err(Error::RowNotFound); + }; + Ok(User { + id: user_id, + name, + team, + }) +} diff --git a/src/application/calendar.rs b/src/application/calendar.rs index 00b79d3..c5b273a 100644 --- a/src/application/calendar.rs +++ b/src/application/calendar.rs @@ -4,10 +4,15 @@ use async_trait::async_trait; use serde::Serialize; use sqlx::Error; +use crate::persistence::{ + member::MemberRepository, talk::TalkRepository, team::TeamRepository, user::UserRepository, +}; + +use super::{user_ids_to_users, User}; + #[async_trait] pub trait CalendarService { - async fn get_all_talks(&self) -> Result, Error>; - async fn get_own_talks(&self, user_id: i64) -> Result, Error>; + async fn get_talks(&self, user_id: Option) -> Result, Error>; } #[derive(Debug, Serialize)] @@ -18,32 +23,77 @@ pub struct Talk { pub scheduled_at: Option, pub duration: Duration, pub location: Option, - pub nerds: Vec, - pub noobs: Vec, + pub nerds: Vec, + pub noobs: Vec, } -#[derive(Debug, Serialize)] -pub struct Member { - pub id: i64, - pub name: String, - pub team: String, +pub struct ProductionCalendarService { + team_repository: TeamRepo, + user_repository: UserRepo, + talk_repository: TalkRepo, + member_repository: MemberRepo, } -pub struct ProductionCalendarService {} - -impl ProductionCalendarService { - pub fn new() -> Self { - Self {} +impl< + TeamRepo: TeamRepository, + UserRepo: UserRepository, + TalkRepo: TalkRepository, + MemberRepo: MemberRepository, + > ProductionCalendarService +{ + pub fn new( + team_repository: TeamRepo, + user_repository: UserRepo, + talk_repository: TalkRepo, + member_repository: MemberRepo, + ) -> Self { + Self { + team_repository, + user_repository, + talk_repository, + member_repository, + } } } #[async_trait] -impl CalendarService for ProductionCalendarService { - async fn get_all_talks(&self) -> Result, Error> { - todo!() - } +impl< + TeamRepo: TeamRepository + Send + Sync, + UserRepo: UserRepository + Send + Sync, + TalkRepo: TalkRepository + Send + Sync, + MemberRepo: MemberRepository + Send + Sync, + > CalendarService for ProductionCalendarService +{ + async fn get_talks(&self, user_id: Option) -> Result, Error> { + let mut talks = Vec::new(); + for talk in self.talk_repository.get_all().await? { + let (nerd_ids, noob_ids) = self + .member_repository + .get_nerds_and_noobs_by_talk(talk.id) + .await?; + + if let Some(user_id) = user_id { + if !nerd_ids.contains(&user_id) && !noob_ids.contains(&user_id) { + continue; + } + } + + let nerds = + user_ids_to_users(nerd_ids, &self.user_repository, &self.team_repository).await?; + let noobs = + user_ids_to_users(noob_ids, &self.user_repository, &self.team_repository).await?; - async fn get_own_talks(&self, user_id: i64) -> Result, Error> { - todo!() + talks.push(Talk { + id: talk.id, + title: talk.title, + description: talk.description, + scheduled_at: talk.scheduled_at, + duration: talk.duration, + location: talk.location, + nerds, + noobs, + }); + } + Ok(talks) } } diff --git a/src/application/talks.rs b/src/application/talks.rs index cbafb84..76f4f73 100644 --- a/src/application/talks.rs +++ b/src/application/talks.rs @@ -15,7 +15,7 @@ use crate::persistence::{ user::UserRepository, }; -use super::authentication::Capability; +use super::{authentication::Capability, user_id_to_user, user_ids_to_users, User}; #[async_trait] pub trait TalksService { @@ -112,13 +112,6 @@ pub struct Talk { noobs: Vec, } -#[derive(Clone, Serialize)] -pub struct User { - id: i64, - name: String, - team: String, -} - pub struct ProductionTalksService { team_repository: TeamRepo, user_repository: UserRepo, @@ -419,33 +412,3 @@ fn get_talk_id_by_command(command: &Command) -> i64 { Command::ToggleNoob { id } => *id, } } - -async fn user_ids_to_users( - user_ids: Vec, - user_repository: &impl UserRepository, - team_repository: &impl TeamRepository, -) -> Result, Error> { - let mut users = Vec::new(); - for user_id in user_ids { - users.push(user_id_to_user(user_id, user_repository, team_repository).await?); - } - Ok(users) -} - -async fn user_id_to_user( - user_id: i64, - user_repository: &impl UserRepository, - team_repository: &impl TeamRepository, -) -> Result { - let Some((name, team_id)) = user_repository.get_name_and_team_id_by_id(user_id).await? else { - return Err(Error::RowNotFound); - }; - let Some(team) = team_repository.get_name_by_id(team_id).await? else { - return Err(Error::RowNotFound); - }; - Ok(User { - id: user_id, - name, - team, - }) -} diff --git a/src/main.rs b/src/main.rs index 2ed2a66..118fde3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,12 @@ async fn main() { SqliteRoleRepository::new(pool.clone()), SqliteTokenRepository::new(pool.clone()), ), - ProductionCalendarService::new(), + ProductionCalendarService::new( + SqliteTeamRepository::new(pool.clone()), + SqliteUserRepository::new(pool.clone()), + SqliteTalkRepository::new(pool.clone()), + SqliteMemberRepository::new(pool.clone()), + ), ProductionTalksService::new( SqliteTeamRepository::new(pool.clone()), SqliteUserRepository::new(pool.clone()), diff --git a/src/presentation.rs b/src/presentation.rs index 910341d..73b9946 100644 --- a/src/presentation.rs +++ b/src/presentation.rs @@ -1,3 +1,4 @@ +mod talks_ics; mod talks_ws; mod teams_json; @@ -15,6 +16,7 @@ use crate::application::{ authentication::AuthenticationService, calendar::CalendarService, talks::TalksService, teams::TeamsService, }; +use talks_ics::talks_ics; use talks_ws::talks_ws; use teams_json::teams_json; @@ -33,7 +35,7 @@ impl ProductionController { Self { router: Router::new() .route("/talks.ws", get(talks_ws)) - // .route("/talks.ics", get(Self::talks_ics)) + .route("/talks.ics", get(talks_ics)) .route("/teams.json", get(teams_json)) .with_state(Arc::new(Services { authentication: authentication_service, diff --git a/src/presentation/talks_ics.rs b/src/presentation/talks_ics.rs new file mode 100644 index 0000000..25a2e91 --- /dev/null +++ b/src/presentation/talks_ics.rs @@ -0,0 +1,109 @@ +use std::{fmt::Write, sync::Arc}; + +use axum::{ + extract::{Query, State}, + http::{header::CONTENT_TYPE, StatusCode}, + response::IntoResponse, +}; +use serde::Deserialize; +use time::{format_description::parse, OffsetDateTime}; + +use crate::application::{ + authentication::AuthenticationService, calendar::CalendarService, talks::TalksService, + teams::TeamsService, +}; + +use super::Services; + +pub async fn talks_ics( + parameters: Query, + State(services): State< + Arc< + Services< + impl AuthenticationService, + impl CalendarService, + impl TalksService, + impl TeamsService, + >, + >, + >, +) -> impl IntoResponse { + let mut response = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//HULKs//mopad//EN\r\nNAME:MOPAD\r\nX-WR-CALNAME:MOPAD\r\nX-WR-CALDESC:Moderated Organization PAD (powerful, agile, distributed)\r\n".to_string(); + let format = parse("[year][month][day]T[hour][minute][second]Z").unwrap(); + let now = OffsetDateTime::now_utc(); + + let talks = match services.calendar.get_talks(parameters.user_id).await { + Ok(talks) => talks, + Err(error) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CONTENT_TYPE, "text/plain; charset=utf-8")], + error.to_string(), + ) + } + }; + + for talk in talks { + if let Some(scheduled_at) = talk.scheduled_at { + let start = OffsetDateTime::from(scheduled_at); + let end = start + talk.duration; + write!( + response, + "BEGIN:VEVENT\r\nUID:{}\r\nDTSTAMP:{}\r\nDTSTART:{}\r\nDTEND:{}\r\nSUMMARY:{}\r\nDESCRIPTION:{}\r\n", + talk.id, + now.format(&format).unwrap(), + start.format(&format).unwrap(), + end.format(&format).unwrap(), + talk.title.replace('\r', "").replace('\n', ""), + talk.description.replace('\r', "").replace('\n', ""), + ) + .unwrap(); + if let Some(location) = &talk.location { + write!( + response, + "LOCATION:{}\r\n", + location.replace('\r', "").replace(';', "") + ) + .unwrap(); + } + for nerd in talk.nerds { + write!( + response, + "ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN={} ({}):MAILTO:user{}@mopad\r\n", + nerd.name + .replace(';', "") + .replace('\r', "") + .replace('\n', ""), + nerd.team + .replace(';', "") + .replace('\r', "") + .replace('\n', ""), + nerd.id, + ) + .unwrap(); + } + for noob in talk.noobs { + write!( + response, + "ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN={} ({}):MAILTO:user{}@mopad\r\n", + noob.name.replace(';', "").replace('\r', "").replace('\n', ""), + noob.team.replace(';', "").replace('\r', "").replace('\n', ""), + noob.id, + ) + .unwrap(); + } + write!(response, "END:VEVENT\r\n",).unwrap(); + } + } + write!(response, "END:VCALENDAR\r\n",).unwrap(); + ( + StatusCode::OK, + [(CONTENT_TYPE, "text/calendar; charset=utf-8")], + response, + ) +} + +#[derive(Deserialize)] +pub struct ICalendarParameters { + user_id: Option, +}