start switching api calls

This commit is contained in:
Cadey Ratio 2020-11-02 13:06:50 -05:00
parent 1366c67d4a
commit e8e80fbedb
14 changed files with 1261 additions and 408 deletions

4
README.md Normal file
View File

@ -0,0 +1,4 @@
# mi
A personal API. This is a port of [mi](https://tulpa.dev/cadey/mi) to Rust and
without elm-spa.

1296
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,32 +3,42 @@ name = "mi"
version = "1.0.0" version = "1.0.0"
authors = ["Christine Dodrill <me@christine.website>"] authors = ["Christine Dodrill <me@christine.website>"]
edition = "2018" edition = "2018"
build = "src/build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
chrono = "0.4" chrono = { version = "0.4", features = ["serde"] }
color-eyre = "0.5.2" color-eyre = "0.5"
diesel = { version = "1.4.4", features = ["sqlite"] } egg-mode = "0.15"
egg-mode = "0.15.0"
elefren = "0.22" elefren = "0.22"
hyper = "0.13"
kankyo = "0.3" kankyo = "0.3"
log = "0.4" log = "0.4"
mime = "0.3.0" mime = "0.3.0"
paseto = "1.0.7" paseto = { version = "1.0", features = ["easy_tokens", "v2"] }
pretty_env_logger = "0" ring = { version = "^0.16", features = ["std"] }
prometheus = { version = "0.10", default-features = false, features = ["process"] }
rand = "0" rand = "0"
rusty_ulid = "0.10.0" rusty_ulid = "0.10"
serde_dhall = "0.6.0" serde_dhall = "0.8"
serde_json = "^1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
thiserror = "1" thiserror = "1"
tokio = { version = "0.2", features = ["macros"] } rocket = "0.4"
warp = "0.2" tracing = "0.1"
tracing-log = "0.1"
tracing-subscriber = "0.2"
ureq = { version = "1", features = ["json", "charset"] }
uuid = { version = "0.7", features = ["serde", "v4"] }
rocket_prometheus = "0.7.0"
prometheus = { version = "0.10", default-features = false, features = ["process"] }
jsonfeed = { git = "https://github.com/Xe/site" } jsonfeed = { git = "https://github.com/Xe/site" }
[build-dependencies] [dependencies.diesel]
ructe = { version = "0.12", features = ["warp02"] } version = "1"
features = ["sqlite", "r2d2", "uuidv07", "chrono"]
[dependencies.rocket_contrib]
version = "0.4"
default-features = false
features = ["json", "diesel_sqlite_pool", "uuid", "helmet"]

View File

@ -1,17 +1,17 @@
PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS members CREATE TABLE IF NOT EXISTS members
( id INT UNIQUE NOT NULL PRIMARY KEY ( id INTEGER UNIQUE NOT NULL PRIMARY KEY
, cmene TEXT UNIQUE NOT NULL , cmene TEXT UNIQUE NOT NULL
, picurl TEXT UNIQUE NOT NULL , picurl TEXT UNIQUE NOT NULL
); );
CREATE TABLE IF NOT EXISTS switches CREATE TABLE IF NOT EXISTS switches
( id TEXT UNIQUE NOT NULL PRIMARY KEY ( id TEXT UNIQUE NOT NULL PRIMARY KEY
, who TEXT NOT NULL , member_id INTEGER NOT NULL
, started_at TEXT NOT NULL , started_at TIMESTAMP NOT NULL
, ended_at TEXT , ended_at TIMESTAMP
, duration TEXT NOT NULL , duration INTEGER NOT NULL
, FOREIGN KEY (who) , FOREIGN KEY (member_id)
REFERENCES members(cmene) REFERENCES members(id)
); );

View File

@ -1,4 +1,3 @@
-- Your SQL goes here
INSERT INTO INSERT INTO
members(id, cmene, picurl) members(id, cmene, picurl)
VALUES VALUES
@ -8,3 +7,10 @@ VALUES
(3, 'Ashe', 'https://mi.within.website/static/img/ashe.png'), (3, 'Ashe', 'https://mi.within.website/static/img/ashe.png'),
(4, 'Sephie', 'https://mi.within.website/static/img/sephie.png'), (4, 'Sephie', 'https://mi.within.website/static/img/sephie.png'),
(5, 'Mai', 'https://mi.within.website/static/img/mai.png'); (5, 'Mai', 'https://mi.within.website/static/img/mai.png');
-- testing values
INSERT INTO
switches(id, member_id, started_at, ended_at, duration)
VALUES
( 'ropjar', 0, '2013-10-07 08:23:19.120', datetime('now'), 200),
( 'flopnax', 5, datetime('now'), NULL, 0)

140
backend/src/api.rs Normal file
View File

@ -0,0 +1,140 @@
use crate::{models, schema, MainDatabase};
use chrono::NaiveDateTime;
use diesel::prelude::*;
use rocket::{
data::{self, FromDataSimple},
http::{ContentType, Status},
request::{self, FromRequest, Request},
response::Responder,
Data,
Outcome::*,
Response,
};
use rocket_contrib::json::Json;
use std::io::Read;
#[get("/members")]
#[instrument(skip(conn), err)]
pub fn get_members(conn: MainDatabase) -> Result<Json<Vec<models::Member>>> {
use schema::members;
let results = members::table
.load::<models::Member>(&*conn)
.map_err(Error::Database)?;
Ok(Json(results))
}
#[derive(serde::Serialize)]
pub struct FrontChange {
pub id: String,
pub who: String, // models::Member.name
pub started_at: NaiveDateTime,
pub ended_at: Option<NaiveDateTime>,
pub duration: i32,
}
#[get("/switches?<count>&<page>")]
#[instrument(skip(conn), err)]
pub fn get_switches(
conn: MainDatabase,
count: Option<i64>,
page: Option<i64>,
) -> Result<Json<Vec<FrontChange>>> {
use schema::{members, switches};
let count = count.unwrap_or(50);
let page = page.unwrap_or(0);
let count = if count < 100 { count } else { 100 };
let result: Vec<FrontChange> = switches::table
.inner_join(members::table)
.order_by(switches::dsl::started_at.desc())
.limit(count)
.offset(count * (page - 1))
.load::<(models::Switch, models::Member)>(&*conn)
.map_err(Error::Database)?
.into_iter()
.map(|front| FrontChange {
id: front.0.id,
who: front.1.cmene,
started_at: front.0.started_at,
ended_at: front.0.ended_at,
duration: front.0.duration,
})
.collect();
match result.len() {
0 => Err(Error::NotFound),
_ => Ok(Json(result)),
}
}
#[get("/switches/current")]
#[instrument(skip(conn), err)]
pub fn get_current_front(conn: MainDatabase) -> Result<Json<FrontChange>> {
use schema::{members, switches};
let mut front: Vec<(models::Switch, models::Member)> = switches::table
.inner_join(members::table)
.order_by(switches::dsl::started_at.desc())
.limit(1)
.load(&*conn)
.map_err(Error::Database)?;
match front.pop() {
Some(front) => Ok(Json(FrontChange {
id: front.0.id,
who: front.1.cmene,
started_at: front.0.started_at,
ended_at: front.0.ended_at,
duration: front.0.duration,
})),
None => Err(Error::NotFound),
}
}
#[post("/switches/switch", data = "<who>")]
#[instrument(skip(conn), err)]
pub fn make_switch(conn: MainDatabase, who: StringBody) -> Result {
info!("got here");
Ok(())
}
#[derive(Debug)]
pub struct StringBody(String);
impl FromDataSimple for StringBody {
type Error = String;
fn from_data(req: &Request, data: Data) -> data::Outcome<Self, String> {
let mut contents = String::new();
if let Err(e) = data.open().take(256).read_to_string(&mut contents) {
return Failure((Status::InternalServerError, format!("{:?}", e)));
}
Success(StringBody(contents))
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("internal database error: {0}")]
Database(#[from] diesel::result::Error),
#[error("not found")]
NotFound,
}
pub type Result<T = ()> = std::result::Result<T, Error>;
impl<'a> Responder<'a> for Error {
fn respond_to(self, _: &Request) -> ::std::result::Result<Response<'a>, Status> {
error!("{}", self);
match self {
Error::NotFound => Err(Status::NotFound),
_ => Err(Status::InternalServerError),
}
}
}

View File

@ -1,12 +0,0 @@
use ructe::{Result, Ructe};
use std::process::Command;
fn main() -> Result<()> {
Ructe::from_env()?.compile_templates("templates")?;
let output = Command::new("git").args(&["rev-parse", "HEAD"]).output().unwrap();
let git_hash = String::from_utf8(output.stdout).unwrap();
println!("cargo:rustc-env=GITHUB_SHA={}", git_hash);
Ok(())
}

View File

@ -1,62 +1,51 @@
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] #[macro_use]
extern crate diesel; extern crate diesel;
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate rocket_contrib;
#[macro_use]
extern crate tracing;
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection; use diesel::sqlite::SqliteConnection;
use hyper::{header::CONTENT_TYPE, Body, Response}; use rocket_contrib::helmet::SpaceHelmet;
use prometheus::{Encoder, TextEncoder}; use rocket_prometheus::PrometheusMetrics;
use warp::Filter;
const APPLICATION_NAME: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); const APPLICATION_NAME: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
pub mod api;
pub mod models; pub mod models;
pub mod schema; pub mod schema;
#[tokio::main] #[database("main_data")]
async fn main() -> Result<()> { pub struct MainDatabase(SqliteConnection);
fn main() -> Result<()> {
let _ = kankyo::init(); let _ = kankyo::init();
pretty_env_logger::init();
color_eyre::install()?; color_eyre::install()?;
tracing_subscriber::fmt::init();
log::info!( info!("{} starting up", APPLICATION_NAME);
"starting up {} commit {}",
APPLICATION_NAME,
env!("GITHUB_SHA")
);
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let prometheus = PrometheusMetrics::with_registry(prometheus::default_registry().clone());
let connection = SqliteConnection::establish(&database_url) rocket::ignite()
.expect(&format!("Error connecting to {}", database_url)); .attach(prometheus.clone())
.attach(MainDatabase::fairing())
let healthcheck = warp::get().and(warp::path(".within").and(warp::path("health")).map(|| "OK")); .attach(SpaceHelmet::default())
let metrics_endpoint = warp::path("metrics").and(warp::path::end()).map(move || { .mount("/metrics", prometheus)
let encoder = TextEncoder::new(); .mount(
let metric_families = prometheus::gather(); "/api",
let mut buffer = vec![]; routes![
encoder.encode(&metric_families, &mut buffer).unwrap(); api::get_members,
Response::builder() api::get_switches,
.status(200) api::get_current_front,
.header(CONTENT_TYPE, encoder.format_type()) api::make_switch
.body(Body::from(buffer)) ],
.unwrap() )
}); .launch();
let site = healthcheck
.or(metrics_endpoint)
.map(|reply| warp::reply::with_header(reply, "X-Clacks-Overhead", "GNU Ashlynn"))
.with(warp::log(APPLICATION_NAME));
warp::serve(site).run(([0, 0, 0, 0], 5000)).await;
Ok(()) Ok(())
} }
fn get_members(connection: &SqliteConnection) -> Result<Vec<models::Member>> {
use schema::members;
let results = members::table
.load::<models::Member>(connection)?;
Ok(results)
}

View File

@ -1,15 +1,22 @@
#[derive(Queryable, Debug)] use crate::schema::*;
use chrono::NaiveDateTime;
use serde::Serialize;
#[derive(Queryable, Debug, Serialize)]
pub struct Member { pub struct Member {
pub id: i32, pub id: i32,
#[serde(rename = "name")]
pub cmene: String, pub cmene: String,
pub picurl: String, pub picurl: String,
} }
#[derive(Queryable)] #[derive(Queryable, Associations)]
#[belongs_to(Member)]
#[table_name = "switches"]
pub struct Switch { pub struct Switch {
pub id: String, pub id: String,
pub who: String, pub member_id: i32,
pub started_at: String, pub started_at: NaiveDateTime,
pub ended_at: Option<String>, pub ended_at: Option<NaiveDateTime>,
pub duration: String, pub duration: i32,
} }

View File

@ -9,14 +9,25 @@ table! {
table! { table! {
switches (id) { switches (id) {
id -> Text, id -> Text,
who -> Text, member_id -> Integer,
started_at -> Text, started_at -> Timestamp,
ended_at -> Nullable<Text>, ended_at -> Nullable<Timestamp>,
duration -> Text, duration -> Integer,
} }
} }
table! {
webmentions (id) {
id -> Text,
source_url -> Text,
target_url -> Text,
}
}
joinable!(switches -> members (member_id));
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(
members, members,
switches, switches,
webmentions,
); );

View File

@ -1 +0,0 @@
@()

10
nix/rust.nix Normal file
View File

@ -0,0 +1,10 @@
{ sources ? import ./sources.nix }:
let
pkgs =
import sources.nixpkgs { overlays = [ (import sources.nixpkgs-mozilla) ]; };
channel = "nightly";
date = "2020-10-25";
targets = [ ];
chan = pkgs.rustChannelOfTargets channel date targets;
in chan

View File

@ -5,10 +5,10 @@
"homepage": "https://github.com/nmattia/niv", "homepage": "https://github.com/nmattia/niv",
"owner": "nmattia", "owner": "nmattia",
"repo": "niv", "repo": "niv",
"rev": "dd13098d01eaa6be68237e7e38f96782b0480755", "rev": "20c899271f288d33114760bc298838575fc6c7f9",
"sha256": "1cfjdbsn0219fjzam1k7nqzkz8fb1ypab50rhyj026qbklqq2kvq", "sha256": "07zswk6dhlydihl9g6skmy52grjvqpra8r98f2dmbgwzc1yhjhxq",
"type": "tarball", "type": "tarball",
"url": "https://github.com/nmattia/niv/archive/dd13098d01eaa6be68237e7e38f96782b0480755.tar.gz", "url": "https://github.com/nmattia/niv/archive/20c899271f288d33114760bc298838575fc6c7f9.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz" "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
}, },
"nixpkgs": { "nixpkgs": {
@ -17,10 +17,10 @@
"homepage": "https://github.com/NixOS/nixpkgs", "homepage": "https://github.com/NixOS/nixpkgs",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs-channels", "repo": "nixpkgs-channels",
"rev": "51428e8d38271d14146211867984b8742d304ea4", "rev": "502845c3e31ef3de0e424f3fcb09217df2ce6df6",
"sha256": "1pphvg9mrbq4vm73xrbdr7970gb389lp3xbnzgyv3gjri2zrkyl9", "sha256": "0fcqpsy6y7dgn0y0wgpa56gsg0b0p8avlpjrd79fp4mp9bl18nda",
"type": "tarball", "type": "tarball",
"url": "https://github.com/NixOS/nixpkgs-channels/archive/51428e8d38271d14146211867984b8742d304ea4.tar.gz", "url": "https://github.com/NixOS/nixpkgs-channels/archive/502845c3e31ef3de0e424f3fcb09217df2ce6df6.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz" "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
}, },
"nixpkgs-mozilla": { "nixpkgs-mozilla": {
@ -29,10 +29,10 @@
"homepage": null, "homepage": null,
"owner": "mozilla", "owner": "mozilla",
"repo": "nixpkgs-mozilla", "repo": "nixpkgs-mozilla",
"rev": "efda5b357451dbb0431f983cca679ae3cd9b9829", "rev": "8c007b60731c07dd7a052cce508de3bb1ae849b4",
"sha256": "11wqrg86g3qva67vnk81ynvqyfj0zxk83cbrf0p9hsvxiwxs8469", "sha256": "1zybp62zz0h077zm2zmqs2wcg3whg6jqaah9hcl1gv4x8af4zhs6",
"type": "tarball", "type": "tarball",
"url": "https://github.com/mozilla/nixpkgs-mozilla/archive/efda5b357451dbb0431f983cca679ae3cd9b9829.tar.gz", "url": "https://github.com/mozilla/nixpkgs-mozilla/archive/8c007b60731c07dd7a052cce508de3bb1ae849b4.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz" "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
} }
} }

View File

@ -2,14 +2,11 @@ let
sources = import ./nix/sources.nix; sources = import ./nix/sources.nix;
pkgs = pkgs =
import sources.nixpkgs { overlays = [ (import sources.nixpkgs-mozilla) ]; }; import sources.nixpkgs { overlays = [ (import sources.nixpkgs-mozilla) ]; };
ruststable = (pkgs.latest.rustChannels.stable.rust.override { rust = import ./nix/rust.nix { };
extensions = [ "rust-src" "rls-preview" "rust-analysis" "rustfmt-preview" ]; in pkgs.mkShell rec {
});
in pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
# rust # rust
ruststable rust
pkgconfig pkgconfig
openssl openssl
cmake cmake
@ -17,6 +14,7 @@ in pkgs.mkShell {
libgit2 libgit2
diesel-cli diesel-cli
sqlite sqlite
cargo-watch
# elm # elm
elmPackages.elm elmPackages.elm
@ -29,5 +27,6 @@ in pkgs.mkShell {
]; ];
DATABASE_URL = "./mi.db"; DATABASE_URL = "./mi.db";
ROCKET_DATABASES = ''{ main_data = { url = "${DATABASE_URL}" } }'';
RUST_LOG = "info"; RUST_LOG = "info";
} }