Compare commits

...

5 Commits

Author SHA1 Message Date
Astro 72d4a34015 serve: colorize sat+sun 2020-10-26 20:14:04 +01:00
Astro b471b76bec style 2020-10-26 19:48:17 +01:00
Astro 9a1a106e0e serve: split main into index 2020-10-26 16:51:25 +01:00
Astro d1566f6c31 serve: switch from rocket to gotham 2020-10-26 16:42:16 +01:00
Astro 650d41b39f nix: remove mozillaOverlay, use stable rust 2020-10-26 14:49:25 +01:00
10 changed files with 1698 additions and 935 deletions

2193
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,4 +3,7 @@ members = [
"ticker-update",
"ticker-serve",
"libticker",
]
]
[patch.crates-io]
typed-html = { git = "https://github.com/bodil/typed-html", rev = "4c13ecca506887d07638cdf12d6ea6d51cd3b29a" }

View File

@ -1,53 +1,22 @@
{ mozillaOverlay ? import <mozillaOverlay>,
rustManifest ? ./channel-rust-nightly.toml,
{ pkgs ? import <nixpkgs> {},
}:
let
pkgs = import <nixpkgs> { overlays = [ mozillaOverlay ]; };
in
cargoSha256 = "11hvxh8drqpimax7b5z5r8qiwzy00j5r0xdshml0wgzxnmrnxzqc";
with pkgs;
let
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";
build = pname: pkgs.rustPlatform.buildRustPackage {
inherit pname;
version = "0.1.0";
src = ./.;
buildInputs = [
pkg-config openssl postgresql.lib
rust
buildInputs = with pkgs; [
pkg-config openssl
postgresql.lib
];
preBuild = "pushd ticker-update";
postBuild = "popd";
inherit cargoSha256;
};
ticker-serve = rustPlatform.buildRustPackage {
name = "ticker-serve";
src = ./.;
buildInputs = [
pkg-config openssl postgresql.lib
rust
];
preBuild = "pushd ticker-serve";
preBuild = "pushd ${pname}";
postBuild = "popd";
inherit cargoSha256;
};
in {
inherit
rustPlatform
ticker-update
ticker-serve;
ticker-update = build "ticker-update";
ticker-serve = build "ticker-serve";
}

View File

@ -3,15 +3,18 @@ use std::collections::BTreeMap;
use serde::{Serialize, Deserialize};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CalendarOptions {
pub url: String,
pub color: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Config {
pub db_url: String,
pub calendars: BTreeMap<String, CalendarOptions>,
pub weekdays: Vec<String>,
pub months: Vec<String>,
}
impl Config {

View File

@ -1,15 +1,15 @@
{ pkgs ? import <nixpkgs> {} }:
with pkgs;
let
default = import ./default.nix {};
default = import ./default.nix { inherit pkgs; };
in
stdenv.mkDerivation {
pkgs.stdenv.mkDerivation {
name = "env";
buildInputs =
default.ticker-update.buildInputs;
[ pkgs.rustPlatform.rust.cargo ] ++
default.ticker-update.buildInputs ++
default.ticker-serve.buildInputs;
shellHook = ''
echo "Run 'cargo build --release'"

View File

@ -5,7 +5,10 @@ authors = ["Astro <astro@spaceboyz.net>"]
edition = "2018"
[dependencies]
rocket = "0.4"
gotham = "0.5"
gotham_derive = "0.5"
http = "0.2"
mime = "0.3"
typed-html = "0.2"
diesel = { version = "~1", features = ["postgres", "chrono"] }
chrono = "~0.4"

136
ticker-serve/src/index.rs Normal file
View File

@ -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)
}

View File

@ -1,117 +1,51 @@
#![feature(proc_macro_hygiene, decl_macro)]
#![recursion_limit="1024"]
use std::sync::Mutex;
#[macro_use] extern crate rocket;
use rocket::{State, response::content};
use typed_html::{html, text, dom::DOMTree};
use diesel::{Connection, pg::PgConnection, prelude::*};
use chrono::{offset::Local, NaiveDate};
#[macro_use]
extern crate gotham_derive;
use libticker::{
config::{Config, CalendarOptions},
schema::{self, events::dsl::events},
model::{Calendar, Event},
ics::{Object, Timestamp, GetValue},
use std::sync::{Arc, Mutex};
use gotham::{
handler::assets::FileOptions,
router::builder::{DefineSingleRoute, DrawRoutes},
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> {
if s.starts_with("http:") || s.starts_with("https:") {
s.into()
} else {
format!("http://{}", s).into()
}
}
use libticker::config::Config;
mod index;
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
}
#[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())
#[derive(Clone, StateData)]
pub struct AppState {
pub db: Arc<Mutex<PgConnection>>,
pub config: Config,
}
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)
.expect("DB");
rocket::ignite()
.manage(Mutex::new(db))
.mount("/", routes![
index,
])
.launch();
let state = AppState {
db: Arc::new(Mutex::new(db)),
config,
};
let (chain, pipelines) = single_pipeline(
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)
}

View File

@ -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;
}

View File

@ -258,7 +258,7 @@ impl std::fmt::Display for Error {
}
fn main() {
let config = Config::read_yaml_file("config.yaml");
let config = Config::read_yaml_file("../config.yaml");
let res = Resources::new(
config.db_url,
config.calendars.into_iter()