Skip to content

Commit

Permalink
Implement iCal support
Browse files Browse the repository at this point in the history
  • Loading branch information
h3ndrk committed Dec 1, 2023
1 parent 45dbb19 commit 2b6c01c
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 60 deletions.
42 changes: 42 additions & 0 deletions src/application.rs
Original file line number Diff line number Diff line change
@@ -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<i64>,
user_repository: &impl UserRepository,
team_repository: &impl TeamRepository,
) -> Result<Vec<User>, 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<User, Error> {
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,
})
}
90 changes: 70 additions & 20 deletions src/application/calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<Talk>, Error>;
async fn get_own_talks(&self, user_id: i64) -> Result<Vec<Talk>, Error>;
async fn get_talks(&self, user_id: Option<i64>) -> Result<Vec<Talk>, Error>;
}

#[derive(Debug, Serialize)]
Expand All @@ -18,32 +23,77 @@ pub struct Talk {
pub scheduled_at: Option<SystemTime>,
pub duration: Duration,
pub location: Option<String>,
pub nerds: Vec<Member>,
pub noobs: Vec<Member>,
pub nerds: Vec<User>,
pub noobs: Vec<User>,
}

#[derive(Debug, Serialize)]
pub struct Member {
pub id: i64,
pub name: String,
pub team: String,
pub struct ProductionCalendarService<TeamRepo, UserRepo, TalkRepo, MemberRepo> {
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<TeamRepo, UserRepo, TalkRepo, MemberRepo>
{
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<Vec<Talk>, Error> {
todo!()
}
impl<
TeamRepo: TeamRepository + Send + Sync,
UserRepo: UserRepository + Send + Sync,
TalkRepo: TalkRepository + Send + Sync,
MemberRepo: MemberRepository + Send + Sync,
> CalendarService for ProductionCalendarService<TeamRepo, UserRepo, TalkRepo, MemberRepo>
{
async fn get_talks(&self, user_id: Option<i64>) -> Result<Vec<Talk>, 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<Vec<Talk>, 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)
}
}
39 changes: 1 addition & 38 deletions src/application/talks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -112,13 +112,6 @@ pub struct Talk {
noobs: Vec<User>,
}

#[derive(Clone, Serialize)]
pub struct User {
id: i64,
name: String,
team: String,
}

pub struct ProductionTalksService<TeamRepo, UserRepo, TalkRepo, MemberRepo> {
team_repository: TeamRepo,
user_repository: UserRepo,
Expand Down Expand Up @@ -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<i64>,
user_repository: &impl UserRepository,
team_repository: &impl TeamRepository,
) -> Result<Vec<User>, 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<User, Error> {
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,
})
}
7 changes: 6 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
4 changes: 3 additions & 1 deletion src/presentation.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod talks_ics;
mod talks_ws;
mod teams_json;

Expand All @@ -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;

Expand All @@ -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,
Expand Down
109 changes: 109 additions & 0 deletions src/presentation/talks_ics.rs
Original file line number Diff line number Diff line change
@@ -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<ICalendarParameters>,
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<i64>,
}

0 comments on commit 2b6c01c

Please sign in to comment.