mirror of
https://github.com/jhbruhn/notes2ics.git
synced 2025-07-01 18:18:49 +00:00
initial commit
This commit is contained in:
commit
497f7027d2
8 changed files with 2627 additions and 0 deletions
122
.github/workflows/release.yaml
vendored
Normal file
122
.github/workflows/release.yaml
vendored
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
name: Create and publish a Docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['main']
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm/v7
|
||||||
|
- linux/arm64
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
-
|
||||||
|
name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
|
-
|
||||||
|
name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
-
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
-
|
||||||
|
name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
-
|
||||||
|
name: Cache Docker layers
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-multi-${{ matrix.platform }}-buildx-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-multi-${{ matrix.platform }}-buildx
|
||||||
|
-
|
||||||
|
name: Build and push by digest
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
#tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
# Note the mode=max here
|
||||||
|
# More: https://github.com/moby/buildkit#--export-cache-options
|
||||||
|
# And: https://github.com/docker/buildx#--cache-tonametypetypekeyvalue
|
||||||
|
cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
|
||||||
|
-
|
||||||
|
name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "/tmp/digests/${digest#sha256:}"
|
||||||
|
-
|
||||||
|
name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: digests
|
||||||
|
path: /tmp/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
- name: Move cache
|
||||||
|
run: |
|
||||||
|
rm -rf /tmp/.buildx-cache
|
||||||
|
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||||
|
merge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Download digests
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: digests
|
||||||
|
path: /tmp/digests
|
||||||
|
-
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
-
|
||||||
|
name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
-
|
||||||
|
name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
-
|
||||||
|
name: Create manifest list and push
|
||||||
|
working-directory: /tmp/digests
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
|
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||||
|
-
|
||||||
|
name: Inspect image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
2065
Cargo.lock
generated
Normal file
2065
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
20
Cargo.toml
Normal file
20
Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
name = "notes2ics"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
warp = "0.3"
|
||||||
|
reqwest = { version = "0.11", features = ["native-tls-vendored", "cookies"] }
|
||||||
|
anyhow = "1"
|
||||||
|
icalendar = { version = "0.15", features = ["chrono-tz"] }
|
||||||
|
chrono = "0"
|
||||||
|
dyn-fmt = "0.4.0"
|
||||||
|
quick-xml = { version = "0.31", features = ["serialize"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
envconfig = "0.10"
|
37
Dockerfile
Normal file
37
Dockerfile
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
FROM --platform=$BUILDPLATFORM rust:1.70 as cross
|
||||||
|
ARG TARGETARCH
|
||||||
|
COPY docker/platform.sh .
|
||||||
|
RUN ./platform.sh # should write /.platform and /.compiler
|
||||||
|
RUN rustup target add $(cat /.platform)
|
||||||
|
RUN apt update && apt-get install -y unzip $(cat /.compiler)
|
||||||
|
|
||||||
|
WORKDIR ./ics-adapter
|
||||||
|
ADD . ./
|
||||||
|
RUN cargo build --release --target $(cat /.platform)
|
||||||
|
RUN cp ./target/$(cat /.platform)/release/ics-adapter /ics-adapter.bin # Get rid of this when build --out is stable
|
||||||
|
|
||||||
|
|
||||||
|
FROM debian:buster-slim
|
||||||
|
ARG APP=/usr/src/app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y ca-certificates tzdata \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV TZ=Etc/UTC \
|
||||||
|
APP_USER=appuser
|
||||||
|
|
||||||
|
RUN groupadd $APP_USER \
|
||||||
|
&& useradd -g $APP_USER $APP_USER \
|
||||||
|
&& mkdir -p ${APP}
|
||||||
|
|
||||||
|
COPY --from=cross /ics-adapter.bin ${APP}/ics-adapter
|
||||||
|
|
||||||
|
RUN chown -R $APP_USER:$APP_USER ${APP}
|
||||||
|
|
||||||
|
USER $APP_USER
|
||||||
|
WORKDIR ${APP}
|
||||||
|
|
||||||
|
CMD ["./ics-adapter"]
|
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
services:
|
||||||
|
notes2ics:
|
||||||
|
image: ghcr.io/jbruhn/notes2ics:main
|
||||||
|
environment:
|
||||||
|
NOTES_USERNAME: <username>
|
||||||
|
NOTES_PASSWORD: <password>
|
||||||
|
NOTES_HOST: domino2.something.de
|
||||||
|
HOST: 0.0.0.0
|
||||||
|
PORT: 3000
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
# http://localhost:3000/calendar/<username>?startDays=-7&endDays=31&filterInvites=true
|
19
docker/platform.sh
Executable file
19
docker/platform.sh
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Used in Docker build to set platform dependent variables
|
||||||
|
|
||||||
|
case $TARGETARCH in
|
||||||
|
|
||||||
|
"amd64")
|
||||||
|
echo "x86_64-unknown-linux-gnu" > /.platform
|
||||||
|
echo "" > /.compiler
|
||||||
|
;;
|
||||||
|
"arm64")
|
||||||
|
echo "aarch64-unknown-linux-gnu" > /.platform
|
||||||
|
echo "gcc-aarch64-linux-gnu" > /.compiler
|
||||||
|
;;
|
||||||
|
"arm")
|
||||||
|
echo "armv7-unknown-linux-gnueabihf" > /.platform
|
||||||
|
echo "gcc-arm-linux-gnueabihf" > /.compiler
|
||||||
|
;;
|
||||||
|
esac
|
351
src/main.rs
Normal file
351
src/main.rs
Normal file
|
@ -0,0 +1,351 @@
|
||||||
|
use dyn_fmt::AsStrFormatExt;
|
||||||
|
use reqwest::ClientBuilder;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::convert::Infallible;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tracing_subscriber::fmt::format::FmtSpan;
|
||||||
|
use warp::{Filter, Rejection, Reply};
|
||||||
|
use envconfig::Envconfig;
|
||||||
|
|
||||||
|
const DATETIME_FORMAT: &str = "%Y%m%dT%H%M%S,00%#z";
|
||||||
|
|
||||||
|
const LOGIN_URL: &str = "names.nsf?Login";
|
||||||
|
const CALENDAR_URL: &str = "mail/{}.nsf/($Calendar)?ReadViewEntries&Count=-1&KeyType=time";
|
||||||
|
// StartKey
|
||||||
|
// UntilKey
|
||||||
|
|
||||||
|
#[derive(Envconfig, Clone)]
|
||||||
|
struct Config {
|
||||||
|
#[envconfig(from = "NOTES_USERNAME")]
|
||||||
|
pub notes_username: String,
|
||||||
|
|
||||||
|
#[envconfig(from = "NOTES_PASSWORD")]
|
||||||
|
pub notes_password: String,
|
||||||
|
|
||||||
|
#[envconfig(from = "NOTES_HOST")]
|
||||||
|
pub notes_host: String,
|
||||||
|
|
||||||
|
#[envconfig(from = "PORT", default = "3000")]
|
||||||
|
pub port: u16,
|
||||||
|
|
||||||
|
#[envconfig(from = "HOST", default = "0.0.0.0")]
|
||||||
|
pub host: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn login(client: &reqwest::Client, host: &str, username: &str, password: &str) -> anyhow::Result<()> {
|
||||||
|
let mut params = HashMap::new();
|
||||||
|
params.insert("Username", username);
|
||||||
|
params.insert("Password", password);
|
||||||
|
let target = format!("/{}", CALENDAR_URL.format(&[username]));
|
||||||
|
//let target = format!("https://{}{}", host, &target.as_str());
|
||||||
|
params.insert("RedirectTo", &target.as_str());
|
||||||
|
let response = client
|
||||||
|
.post(format!("https://{}/{}", host, LOGIN_URL))
|
||||||
|
.form(¶ms)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let logged_in = response?.status() == 302; // redirecting to target ressource
|
||||||
|
if logged_in {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("Invalid credentials!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_calendar(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
host: &str,
|
||||||
|
username: &str,
|
||||||
|
days_back: i64,
|
||||||
|
days_ahead: i64,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
let start =
|
||||||
|
(chrono::Utc::now() + chrono::Duration::days(days_back)).format("%Y%m%dT000001,00Z");
|
||||||
|
let end = (chrono::Utc::now() + chrono::Duration::days(days_ahead)).format("%Y%m%dT000001,00Z");
|
||||||
|
let target = CALENDAR_URL.format(&[username]);
|
||||||
|
let target = format!(
|
||||||
|
"https://{}/{}&StartKey={}&UntilKey={}",
|
||||||
|
host,
|
||||||
|
&target.as_str(),
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
);
|
||||||
|
let response = client.get(target).send().await?;
|
||||||
|
let text = response.text().await?;
|
||||||
|
let logged_in = text.contains("viewentries");
|
||||||
|
if logged_in {
|
||||||
|
Ok(text)
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("Not logged in perhaps?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct CustomReject(anyhow::Error);
|
||||||
|
|
||||||
|
impl warp::reject::Reject for CustomReject {}
|
||||||
|
|
||||||
|
pub(crate) fn custom_reject(error: impl Into<anyhow::Error>) -> warp::Rejection {
|
||||||
|
warp::reject::custom(CustomReject(error.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handler(
|
||||||
|
calendar_user: String,
|
||||||
|
client: Arc<reqwest::Client>,
|
||||||
|
config: Config,
|
||||||
|
params: HashMap<String, String>,
|
||||||
|
) -> anyhow::Result<impl Reply, Rejection> {
|
||||||
|
let days_back = params
|
||||||
|
.get("startDays")
|
||||||
|
.and_then(|x| x.parse().ok())
|
||||||
|
.unwrap_or(-14);
|
||||||
|
let days_ahead = params
|
||||||
|
.get("endDays")
|
||||||
|
.and_then(|x| x.parse().ok())
|
||||||
|
.unwrap_or(31);
|
||||||
|
let filter_invites = params
|
||||||
|
.get("filterInvites")
|
||||||
|
.and_then(|x| x.parse().ok())
|
||||||
|
.unwrap_or(true);
|
||||||
|
let calendar_str = load_calendar(&client, &config.notes_host, &calendar_user, days_back, days_ahead).await;
|
||||||
|
if let Ok(calendar_str) = calendar_str {
|
||||||
|
tracing::info!("Got calendar for {}", calendar_user);
|
||||||
|
Ok(parse_calendar(&calendar_str, filter_invites)
|
||||||
|
.map_err(custom_reject)?
|
||||||
|
.to_string())
|
||||||
|
} else {
|
||||||
|
tracing::info!("Logging in again");
|
||||||
|
login(&client, &config.notes_host, &config.notes_username, &config.notes_password)
|
||||||
|
.await
|
||||||
|
.map_err(custom_reject)?;
|
||||||
|
tracing::info!("Logged in, loading calendar");
|
||||||
|
Ok(parse_calendar(
|
||||||
|
&load_calendar(&client, &config.notes_host, &calendar_user, days_back, days_ahead)
|
||||||
|
.await
|
||||||
|
.map_err(custom_reject)?,
|
||||||
|
filter_invites,
|
||||||
|
)
|
||||||
|
.map_err(custom_reject)?
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_client(
|
||||||
|
client: Arc<reqwest::Client>,
|
||||||
|
) -> impl Filter<Extract = (Arc<reqwest::Client>,), Error = Infallible> + Clone {
|
||||||
|
warp::any().map(move || client.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_config(
|
||||||
|
config: Config
|
||||||
|
) -> impl Filter<Extract = (Config,), Error = Infallible> + Clone {
|
||||||
|
warp::any().map(move || config.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let filter = std::env::var("RUST_LOG")
|
||||||
|
.unwrap_or_else(|_| "notes2ics=debug,tracing=info,warp=debug".to_owned());
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(filter)
|
||||||
|
.with_span_events(FmtSpan::CLOSE)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let config = Config::init_from_env().unwrap();
|
||||||
|
|
||||||
|
let client = ClientBuilder::new()
|
||||||
|
.cookie_store(true)
|
||||||
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let client = Arc::new(client);
|
||||||
|
let get_calendar_route = warp::path!("calendar" / String)
|
||||||
|
.and(with_client(client))
|
||||||
|
.and(with_config(config.clone()))
|
||||||
|
.and(warp::query::<HashMap<String, String>>())
|
||||||
|
.and_then(handler);
|
||||||
|
warp::serve(
|
||||||
|
warp::get()
|
||||||
|
.and(get_calendar_route)
|
||||||
|
.with(warp::trace::request()),
|
||||||
|
)
|
||||||
|
.run((config.host.parse::<std::net::Ipv4Addr>().unwrap(), config.port))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct CalendarDocument {
|
||||||
|
#[serde(rename = "@timestamp")]
|
||||||
|
_timestamp: String,
|
||||||
|
#[serde(rename = "$value", default)]
|
||||||
|
viewentries: Vec<ViewEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct ViewEntry {
|
||||||
|
#[serde(rename = "$value", default)]
|
||||||
|
entry_datas: Vec<EntryData>,
|
||||||
|
#[serde(rename = "@unid", default)]
|
||||||
|
uid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct EntryData {
|
||||||
|
#[serde(rename = "@name")]
|
||||||
|
name: String,
|
||||||
|
#[serde(rename = "$value")]
|
||||||
|
data: DataList,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
enum DataList {
|
||||||
|
#[serde(rename = "datetimelist")]
|
||||||
|
DateTimeList {
|
||||||
|
#[serde(rename = "$value")]
|
||||||
|
datetimes: Vec<DataList>,
|
||||||
|
},
|
||||||
|
#[serde(rename = "numberlist")]
|
||||||
|
NumberList {
|
||||||
|
#[serde(rename = "$value")]
|
||||||
|
_number: Vec<DataList>,
|
||||||
|
},
|
||||||
|
#[serde(rename = "textlist")]
|
||||||
|
TextList {
|
||||||
|
#[serde(rename = "$value")]
|
||||||
|
texts: Vec<DataList>,
|
||||||
|
},
|
||||||
|
#[serde(rename = "datetime")]
|
||||||
|
DateTime(String),
|
||||||
|
#[serde(rename = "number")]
|
||||||
|
Number(i32),
|
||||||
|
#[serde(rename = "text")]
|
||||||
|
Text(String),
|
||||||
|
#[serde(rename = "$text")]
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataList {
|
||||||
|
fn try_first_text(&self, skip: usize) -> anyhow::Result<String> {
|
||||||
|
Ok(match self {
|
||||||
|
Self::Text(t) if skip == 0 => t.to_string(),
|
||||||
|
Self::TextList { texts } => texts
|
||||||
|
.iter()
|
||||||
|
.skip(skip)
|
||||||
|
.next()
|
||||||
|
.ok_or(anyhow::anyhow!("empty"))?
|
||||||
|
.try_first_text(0)?,
|
||||||
|
_ => anyhow::bail!("Wrong format"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_first_datetime(&self, skip: usize) -> anyhow::Result<String> {
|
||||||
|
Ok(match self {
|
||||||
|
Self::DateTime(v) if skip == 0 => v.to_string(),
|
||||||
|
Self::DateTimeList { datetimes } => datetimes
|
||||||
|
.iter()
|
||||||
|
.skip(skip)
|
||||||
|
.next()
|
||||||
|
.ok_or(anyhow::anyhow!("empty"))?
|
||||||
|
.try_first_datetime(0)?,
|
||||||
|
_ => anyhow::bail!("Wrong format"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_calendar(text: &str, filter_invites: bool) -> anyhow::Result<icalendar::Calendar> {
|
||||||
|
let doc: CalendarDocument = quick_xml::de::from_str(text)?;
|
||||||
|
|
||||||
|
let mut calendar = icalendar::Calendar::default();
|
||||||
|
|
||||||
|
for view_entry in doc.viewentries {
|
||||||
|
let name = view_entry
|
||||||
|
.entry_datas
|
||||||
|
.iter()
|
||||||
|
.filter(|x| x.name == "$147")
|
||||||
|
.next()
|
||||||
|
.ok_or(anyhow::anyhow!("No summary"))
|
||||||
|
.and_then(|x| x.data.try_first_text(0));
|
||||||
|
let location = view_entry
|
||||||
|
.entry_datas
|
||||||
|
.iter()
|
||||||
|
.filter(|x| x.name == "$147")
|
||||||
|
.next()
|
||||||
|
.ok_or(anyhow::anyhow!("No summary"))
|
||||||
|
.and_then(|x| x.data.try_first_text(1));
|
||||||
|
let start = view_entry
|
||||||
|
.entry_datas
|
||||||
|
.iter()
|
||||||
|
.filter(|x| x.name == "$144")
|
||||||
|
.next()
|
||||||
|
.ok_or(anyhow::anyhow!("No Start"))
|
||||||
|
.and_then(|x| x.data.try_first_datetime(0));
|
||||||
|
let end = view_entry
|
||||||
|
.entry_datas
|
||||||
|
.iter()
|
||||||
|
.filter(|x| x.name == "$146")
|
||||||
|
.next()
|
||||||
|
.ok_or(anyhow::anyhow!("No End"))
|
||||||
|
.and_then(|x| x.data.try_first_datetime(0));
|
||||||
|
let day = view_entry
|
||||||
|
.entry_datas
|
||||||
|
.iter().filter(|x| x.name == "$134")
|
||||||
|
.next()
|
||||||
|
.ok_or(anyhow::anyhow!("No Day"))
|
||||||
|
.and_then(|x| x.data.try_first_datetime(0));
|
||||||
|
|
||||||
|
use icalendar::Component;
|
||||||
|
use icalendar::EventLike;
|
||||||
|
let entry: anyhow::Result<icalendar::Event> = (|| {
|
||||||
|
let day = day?;
|
||||||
|
let mut new_entry = icalendar::Event::new();
|
||||||
|
new_entry.uid(&format!("{}-{}", &view_entry.uid, &day)); // Repeat events
|
||||||
|
// get the same uid,
|
||||||
|
// but we don't know
|
||||||
|
// about the repeats here
|
||||||
|
// so every date is unique.
|
||||||
|
let summary = name?;
|
||||||
|
if filter_invites && summary.starts_with("Einladung: ") {
|
||||||
|
anyhow::bail!("Not accepted");
|
||||||
|
}
|
||||||
|
new_entry.summary(&summary);
|
||||||
|
if let Ok(start) = start {
|
||||||
|
let end = end?;
|
||||||
|
new_entry.starts::<icalendar::CalendarDateTime>(
|
||||||
|
chrono::DateTime::parse_from_str(&start, DATETIME_FORMAT)?
|
||||||
|
.with_timezone(&chrono::Utc)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
new_entry.ends::<icalendar::CalendarDateTime>(
|
||||||
|
chrono::DateTime::parse_from_str(&end, DATETIME_FORMAT)?
|
||||||
|
.with_timezone(&chrono::Utc)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Seems to be all day
|
||||||
|
|
||||||
|
new_entry.starts::<chrono::NaiveDate>(
|
||||||
|
chrono::DateTime::parse_from_str(&day, DATETIME_FORMAT)?
|
||||||
|
.with_timezone(&chrono::Utc)
|
||||||
|
.date_naive()
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(location) = location {
|
||||||
|
new_entry.location(&location);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(new_entry.done())
|
||||||
|
})();
|
||||||
|
if let Ok(entry) = entry {
|
||||||
|
calendar.push(entry);
|
||||||
|
} else {
|
||||||
|
tracing::warn!("Invalid ViewEntry: {:?}", entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(calendar)
|
||||||
|
}
|
Loading…
Reference in a new issue