Compare commits
5 Commits
931cea5968
...
72d4a34015
Author | SHA1 | Date |
---|---|---|
Astro | 72d4a34015 | |
Astro | b471b76bec | |
Astro | 9a1a106e0e | |
Astro | d1566f6c31 | |
Astro | 650d41b39f |
File diff suppressed because it is too large
Load Diff
|
@ -4,3 +4,6 @@ members = [
|
||||||
"ticker-serve",
|
"ticker-serve",
|
||||||
"libticker",
|
"libticker",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
typed-html = { git = "https://github.com/bodil/typed-html", rev = "4c13ecca506887d07638cdf12d6ea6d51cd3b29a" }
|
||||||
|
|
53
default.nix
53
default.nix
|
@ -1,53 +1,22 @@
|
||||||
{ mozillaOverlay ? import <mozillaOverlay>,
|
{ pkgs ? import <nixpkgs> {},
|
||||||
rustManifest ? ./channel-rust-nightly.toml,
|
|
||||||
}:
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
pkgs = import <nixpkgs> { overlays = [ mozillaOverlay ]; };
|
cargoSha256 = "11hvxh8drqpimax7b5z5r8qiwzy00j5r0xdshml0wgzxnmrnxzqc";
|
||||||
in
|
|
||||||
|
|
||||||
with pkgs;
|
build = pname: pkgs.rustPlatform.buildRustPackage {
|
||||||
|
inherit pname;
|
||||||
let
|
version = "0.1.0";
|
||||||
rustChannelOfTargets = _channel: _date:
|
|
||||||
(pkgs.lib.rustLib.fromManifestFile rustManifest {
|
|
||||||
inherit (pkgs) stdenv fetchurl patchelf;
|
|
||||||
}).rust;
|
|
||||||
rust =
|
|
||||||
rustChannelOfTargets "nightly" null;
|
|
||||||
rustPlatform = pkgs.recurseIntoAttrs (pkgs.makeRustPlatform {
|
|
||||||
rustc = rust;
|
|
||||||
cargo = rust;
|
|
||||||
});
|
|
||||||
|
|
||||||
cargoSha256 = "0jbpwr84ncfj5nigznr06fpwaj89n839534n81hqwacflh1z43ix";
|
|
||||||
|
|
||||||
ticker-update = rustPlatform.buildRustPackage {
|
|
||||||
name = "ticker-update";
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
buildInputs = [
|
buildInputs = with pkgs; [
|
||||||
pkg-config openssl postgresql.lib
|
pkg-config openssl
|
||||||
rust
|
postgresql.lib
|
||||||
];
|
];
|
||||||
preBuild = "pushd ticker-update";
|
preBuild = "pushd ${pname}";
|
||||||
postBuild = "popd";
|
|
||||||
inherit cargoSha256;
|
|
||||||
};
|
|
||||||
|
|
||||||
ticker-serve = rustPlatform.buildRustPackage {
|
|
||||||
name = "ticker-serve";
|
|
||||||
src = ./.;
|
|
||||||
buildInputs = [
|
|
||||||
pkg-config openssl postgresql.lib
|
|
||||||
rust
|
|
||||||
];
|
|
||||||
preBuild = "pushd ticker-serve";
|
|
||||||
postBuild = "popd";
|
postBuild = "popd";
|
||||||
inherit cargoSha256;
|
inherit cargoSha256;
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
inherit
|
ticker-update = build "ticker-update";
|
||||||
rustPlatform
|
ticker-serve = build "ticker-serve";
|
||||||
ticker-update
|
|
||||||
ticker-serve;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,18 @@ use std::collections::BTreeMap;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct CalendarOptions {
|
pub struct CalendarOptions {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
pub color: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub db_url: String,
|
pub db_url: String,
|
||||||
pub calendars: BTreeMap<String, CalendarOptions>,
|
pub calendars: BTreeMap<String, CalendarOptions>,
|
||||||
|
pub weekdays: Vec<String>,
|
||||||
|
pub months: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
|
10
shell.nix
10
shell.nix
|
@ -1,15 +1,15 @@
|
||||||
{ pkgs ? import <nixpkgs> {} }:
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
|
||||||
with pkgs;
|
|
||||||
|
|
||||||
let
|
let
|
||||||
default = import ./default.nix {};
|
default = import ./default.nix { inherit pkgs; };
|
||||||
in
|
in
|
||||||
|
|
||||||
stdenv.mkDerivation {
|
pkgs.stdenv.mkDerivation {
|
||||||
name = "env";
|
name = "env";
|
||||||
buildInputs =
|
buildInputs =
|
||||||
default.ticker-update.buildInputs;
|
[ pkgs.rustPlatform.rust.cargo ] ++
|
||||||
|
default.ticker-update.buildInputs ++
|
||||||
|
default.ticker-serve.buildInputs;
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
echo "Run 'cargo build --release'"
|
echo "Run 'cargo build --release'"
|
||||||
|
|
|
@ -5,7 +5,10 @@ authors = ["Astro <astro@spaceboyz.net>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = "0.4"
|
gotham = "0.5"
|
||||||
|
gotham_derive = "0.5"
|
||||||
|
http = "0.2"
|
||||||
|
mime = "0.3"
|
||||||
typed-html = "0.2"
|
typed-html = "0.2"
|
||||||
diesel = { version = "~1", features = ["postgres", "chrono"] }
|
diesel = { version = "~1", features = ["postgres", "chrono"] }
|
||||||
chrono = "~0.4"
|
chrono = "~0.4"
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use gotham::{
|
||||||
|
helpers::http::response::create_response,
|
||||||
|
hyper::{Body, Response},
|
||||||
|
state::{FromState, State},
|
||||||
|
};
|
||||||
|
use http::status::StatusCode;
|
||||||
|
use mime::TEXT_HTML;
|
||||||
|
|
||||||
|
use typed_html::{html, text, dom::DOMTree, types::{Class, SpacedSet}};
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use chrono::{offset::Local, Datelike, NaiveDate};
|
||||||
|
|
||||||
|
use libticker::{
|
||||||
|
schema::{self, events::dsl::events},
|
||||||
|
model::Event,
|
||||||
|
};
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
|
||||||
|
fn fix_url(s: &str) -> std::borrow::Cow<str> {
|
||||||
|
if s.starts_with("http:") || s.starts_with("https:") {
|
||||||
|
s.into()
|
||||||
|
} else {
|
||||||
|
format!("http://{}", s).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DayEvents<'e> {
|
||||||
|
date: NaiveDate,
|
||||||
|
events: &'e [Event],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// assumes pre-sorted input
|
||||||
|
fn group_by_day(es: &[Event]) -> Vec<DayEvents> {
|
||||||
|
let mut results = vec![];
|
||||||
|
|
||||||
|
let mut prev_date = None;
|
||||||
|
let mut date_start = 0;
|
||||||
|
for (i, event) in es.iter().enumerate() {
|
||||||
|
if prev_date.is_some() && prev_date != Some(event.dtstart.date()) {
|
||||||
|
if i > date_start {
|
||||||
|
results.push(DayEvents {
|
||||||
|
date: prev_date.unwrap().clone(),
|
||||||
|
events: &es[date_start..i],
|
||||||
|
});
|
||||||
|
date_start = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prev_date = Some(event.dtstart.date());
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_index(app_state: &AppState) -> String {
|
||||||
|
let db = app_state.db.lock().unwrap();
|
||||||
|
let today = Local::today().naive_local().and_hms(0, 0, 0);
|
||||||
|
let es = events
|
||||||
|
.filter(schema::events::dtstart.ge(&today))
|
||||||
|
.order_by(schema::events::dtstart.asc())
|
||||||
|
.then_order_by(schema::events::dtend.desc())
|
||||||
|
.load::<Event>(&*db)
|
||||||
|
.unwrap();
|
||||||
|
let days = group_by_day(&es);
|
||||||
|
|
||||||
|
let config = &app_state.config;
|
||||||
|
|
||||||
|
let doc: DOMTree<String> = html!(
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>"Ticker"</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="stylesheet" title="Style" type="text/css" href="static/style.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{ days.iter().map(|day| {
|
||||||
|
let mut day_class: SpacedSet<Class> = ["date"].try_into().unwrap();
|
||||||
|
day_class.add(&format!("wd{}", day.date.weekday().num_days_from_monday())[..]);
|
||||||
|
html!(
|
||||||
|
<div>
|
||||||
|
<h2>
|
||||||
|
<span class={ day_class }>
|
||||||
|
<span class="day">
|
||||||
|
{ text!("{}", day.date.day()) }
|
||||||
|
</span>
|
||||||
|
<span class="month">
|
||||||
|
{ text!("{}", &config.months[day.date.month0() as usize]) }
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="weekday">
|
||||||
|
{ text!("{}", &config.weekdays[day.date.weekday().num_days_from_monday() as usize]) }
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{ day.events.iter().map(|e| html!(
|
||||||
|
<article class="event" style={ format!("border-left: 1.5rem solid {}", &config.calendars.get(&e.calendar).map(|o| &o.color[..]).unwrap_or("white")) }>
|
||||||
|
{ match &e.url {
|
||||||
|
None => html!(
|
||||||
|
<h3>{ text!("{}", &e.summary) }</h3>
|
||||||
|
),
|
||||||
|
Some(url) => html!(
|
||||||
|
<h3>
|
||||||
|
<a href={ fix_url(url) }>
|
||||||
|
{ text!("{}", &e.summary) }
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
),
|
||||||
|
} }
|
||||||
|
|
||||||
|
<p class="dtstart" title={ format!("{}", e.dtstart.format("%c")) }>
|
||||||
|
{ text!("{}", &e.dtstart.format("%H:%S")) }
|
||||||
|
</p>
|
||||||
|
{ e.location.as_ref().map(|location| html!(
|
||||||
|
<p class="location">
|
||||||
|
{ text!("{}", location) }
|
||||||
|
</p>
|
||||||
|
)) }
|
||||||
|
</article>
|
||||||
|
)) }
|
||||||
|
</div>)
|
||||||
|
}) }
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
format!("<DOCTYPE html>\n{}", doc.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index(state: State) -> (State, Response<Body>) {
|
||||||
|
let message = {
|
||||||
|
let app_state = AppState::borrow_from(&state);
|
||||||
|
render_index(app_state)
|
||||||
|
};
|
||||||
|
let res = create_response(&state, StatusCode::OK, TEXT_HTML, message);
|
||||||
|
(state, res)
|
||||||
|
}
|
|
@ -1,117 +1,51 @@
|
||||||
#![feature(proc_macro_hygiene, decl_macro)]
|
|
||||||
#![recursion_limit="1024"]
|
#![recursion_limit="1024"]
|
||||||
|
|
||||||
use std::sync::Mutex;
|
#[macro_use]
|
||||||
#[macro_use] extern crate rocket;
|
extern crate gotham_derive;
|
||||||
use rocket::{State, response::content};
|
|
||||||
use typed_html::{html, text, dom::DOMTree};
|
|
||||||
use diesel::{Connection, pg::PgConnection, prelude::*};
|
|
||||||
use chrono::{offset::Local, NaiveDate};
|
|
||||||
|
|
||||||
use libticker::{
|
use std::sync::{Arc, Mutex};
|
||||||
config::{Config, CalendarOptions},
|
use gotham::{
|
||||||
schema::{self, events::dsl::events},
|
handler::assets::FileOptions,
|
||||||
model::{Calendar, Event},
|
router::builder::{DefineSingleRoute, DrawRoutes},
|
||||||
ics::{Object, Timestamp, GetValue},
|
middleware::state::StateMiddleware,
|
||||||
|
pipeline::single::single_pipeline,
|
||||||
|
pipeline::single_middleware,
|
||||||
|
router::builder::*,
|
||||||
};
|
};
|
||||||
|
use diesel::{Connection, pg::PgConnection};
|
||||||
|
|
||||||
fn fix_url(s: &str) -> std::borrow::Cow<str> {
|
use libticker::config::Config;
|
||||||
if s.starts_with("http:") || s.starts_with("https:") {
|
mod index;
|
||||||
s.into()
|
|
||||||
} else {
|
|
||||||
format!("http://{}", s).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DayEvents<'e> {
|
#[derive(Clone, StateData)]
|
||||||
date: NaiveDate,
|
pub struct AppState {
|
||||||
events: &'e [Event],
|
pub db: Arc<Mutex<PgConnection>>,
|
||||||
}
|
pub config: Config,
|
||||||
|
|
||||||
/// assumes pre-sorted input
|
|
||||||
fn group_by_day(es: &[Event]) -> Vec<DayEvents> {
|
|
||||||
let mut results = vec![];
|
|
||||||
|
|
||||||
let mut prev_date = None;
|
|
||||||
let mut date_start = 0;
|
|
||||||
for (i, event) in es.iter().enumerate() {
|
|
||||||
if prev_date.is_some() && prev_date != Some(event.dtstart.date()) {
|
|
||||||
if i > date_start {
|
|
||||||
results.push(DayEvents {
|
|
||||||
date: prev_date.unwrap().clone(),
|
|
||||||
events: &es[date_start..i],
|
|
||||||
});
|
|
||||||
date_start = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prev_date = Some(event.dtstart.date());
|
|
||||||
}
|
|
||||||
|
|
||||||
results
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
fn index(db: State<Mutex<PgConnection>>) -> content::Html<String> {
|
|
||||||
let db = db.lock().unwrap();
|
|
||||||
let today = Local::today().naive_local().and_hms(0, 0, 0);
|
|
||||||
let es = events
|
|
||||||
.filter(schema::events::dtstart.ge(&today))
|
|
||||||
.order_by(schema::events::dtstart.asc())
|
|
||||||
.then_order_by(schema::events::dtend.desc())
|
|
||||||
.load::<Event>(&*db)
|
|
||||||
.unwrap();
|
|
||||||
let days = group_by_day(&es);
|
|
||||||
|
|
||||||
let doc: DOMTree<String> = html!(
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>"Ticker"</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>"Ticker"</h1>
|
|
||||||
|
|
||||||
{ days.iter().map(|day| html!(<div>
|
|
||||||
<nav><h2>{ text!("{}", &day.date) }</h2></nav>
|
|
||||||
|
|
||||||
{ day.events.iter().map(|e| html!(
|
|
||||||
<article class="event">
|
|
||||||
{ match &e.url {
|
|
||||||
None => html!(
|
|
||||||
<h3>{ text!("{}", &e.summary) }</h3>
|
|
||||||
),
|
|
||||||
Some(url) => html!(
|
|
||||||
<h3>
|
|
||||||
<a href={ fix_url(url) }>
|
|
||||||
{ text!("{}", &e.summary) }
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
),
|
|
||||||
} }
|
|
||||||
|
|
||||||
<p class="dtstart">{ text!("{}", &e.dtstart) }</p>
|
|
||||||
{ e.location.as_ref().map(|location| html!(
|
|
||||||
<p>
|
|
||||||
{ text!("{}", location) }
|
|
||||||
</p>
|
|
||||||
)) }
|
|
||||||
</article>
|
|
||||||
)) }
|
|
||||||
</div>)) }
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
content::Html(doc.to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let config = Config::read_yaml_file("config.yaml");
|
let config = Config::read_yaml_file("../config.yaml");
|
||||||
let db = PgConnection::establish(&config.db_url)
|
let db = PgConnection::establish(&config.db_url)
|
||||||
.expect("DB");
|
.expect("DB");
|
||||||
|
|
||||||
rocket::ignite()
|
let state = AppState {
|
||||||
.manage(Mutex::new(db))
|
db: Arc::new(Mutex::new(db)),
|
||||||
.mount("/", routes![
|
config,
|
||||||
index,
|
};
|
||||||
])
|
let (chain, pipelines) = single_pipeline(
|
||||||
.launch();
|
single_middleware(
|
||||||
|
StateMiddleware::new(state)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let router = build_router(chain, pipelines, |route| {
|
||||||
|
route.get("/").to(index::index);
|
||||||
|
route.get("static/*").to_dir(
|
||||||
|
FileOptions::new(&"static")
|
||||||
|
// TODO:
|
||||||
|
.with_cache_control("no-cache")
|
||||||
|
.with_gzip(true)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
gotham::start("[::1]:8400", router)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem 0;
|
||||||
|
background-color: #373737;
|
||||||
|
color: #e7e7e7;
|
||||||
|
max-width: 40rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
border: 1px solid black;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #444;
|
||||||
|
font-weight: 900;
|
||||||
|
width: 4rem;
|
||||||
|
margin-left: -1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
width: 4rem;
|
||||||
|
}
|
||||||
|
/* saturday */
|
||||||
|
.date.wd5 {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
/* sunday */
|
||||||
|
.date.wd6 {
|
||||||
|
color: #d33;
|
||||||
|
}
|
||||||
|
.date .day {
|
||||||
|
align-self: center;
|
||||||
|
font-size: 140%;
|
||||||
|
}
|
||||||
|
.date .month {
|
||||||
|
align-self: center;
|
||||||
|
font-size: 50%;
|
||||||
|
font-variant-caps: small-caps;
|
||||||
|
}
|
||||||
|
.weekday {
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
color: #222;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.8rem 0 0.8rem 1rem;
|
||||||
|
clear: both;
|
||||||
|
line-height: 1.3rem;
|
||||||
|
/*box-shadow: 0 -0.3rem 0.5rem 0.5rem #373737;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
article p {
|
||||||
|
margin: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 a {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dtstart {
|
||||||
|
margin-right: 1.3em;
|
||||||
|
}
|
||||||
|
.dtstart, .location {
|
||||||
|
line-height: 1.1em;
|
||||||
|
display: inline;
|
||||||
|
}
|
|
@ -258,7 +258,7 @@ impl std::fmt::Display for Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let config = Config::read_yaml_file("config.yaml");
|
let config = Config::read_yaml_file("../config.yaml");
|
||||||
let res = Resources::new(
|
let res = Resources::new(
|
||||||
config.db_url,
|
config.db_url,
|
||||||
config.calendars.into_iter()
|
config.calendars.into_iter()
|
||||||
|
|
Loading…
Reference in New Issue