Browse Source

Works without the index page, but I'm going to need to make some big changes for that.

master
Ashley 1 year ago
commit
767beedd50
12 changed files with 2413 additions and 0 deletions
  1. 4
    0
      .gitignore
  2. 1864
    0
      Cargo.lock
  3. 19
    0
      Cargo.toml
  4. 3
    0
      Settings.toml
  5. 6
    0
      build.rs
  6. 19
    0
      log/requests.log
  7. 34
    0
      log4rs.yaml
  8. 173
    0
      src/controller.rs
  9. 72
    0
      src/images.rs
  10. 56
    0
      src/main.rs
  11. 43
    0
      src/rand_iter.rs
  12. 120
    0
      templates/index.html

+ 4
- 0
.gitignore View File

@@ -0,0 +1,4 @@

/target/
**/*.rs.bk
/content/

+ 1864
- 0
Cargo.lock
File diff suppressed because it is too large
View File


+ 19
- 0
Cargo.toml View File

@@ -0,0 +1,19 @@
[package]
name = "shibeapp"
version = "0.1.0"
authors = ["Andrew Rogers <andrew@cpancake.me>"]
build = "build.rs"

[build-dependencies]
askama = "0.7"

[dependencies]
actix-web = "0.7"
glob = "0.2"
serde_json = "1.0"
rand = "0.5.5"
num = "0.2.0"
config = "0.9"
log4rs = "0.8"
log = "0.4.4"
askama = "0.7"

+ 3
- 0
Settings.toml View File

@@ -0,0 +1,3 @@
base_http_url="http://cdn.shibe.online/"
base_https_url="https://cdn.shibe.online/"
port = 6767

+ 6
- 0
build.rs View File

@@ -0,0 +1,6 @@
extern crate askama;

fn main()
{
askama::rerun_if_templates_changed();
}

+ 19
- 0
log/requests.log View File

@@ -0,0 +1,19 @@
2018-08-21T06:25:20.020799500-04:00 - 127.0.0.1:54031 "GET /api/shibes HTTP/1.1" 404 17 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.000999
2018-08-21T06:25:58.440101200-04:00 - 127.0.0.1:54145 "GET /api/shibes HTTP/1.1" 200 79 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.000999
2018-08-21T06:26:01.035431900-04:00 - 127.0.0.1:54145 "GET /api/shibes HTTP/1.1" 200 79 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.000998
2018-08-21T06:26:01.536796-04:00 - 127.0.0.1:54145 "GET /api/shibes HTTP/1.1" 200 79 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.000999
2018-08-21T06:26:01.901453600-04:00 - 127.0.0.1:54145 "GET /api/shibes HTTP/1.1" 200 79 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.001000
2018-08-21T06:26:02.286614300-04:00 - 127.0.0.1:54145 "GET /api/shibes HTTP/1.1" 200 79 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.000999
2018-08-21T06:26:02.641512700-04:00 - 127.0.0.1:54145 "GET /api/shibes HTTP/1.1" 200 79 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.001000
2018-08-21T06:26:02.879385500-04:00 - 127.0.0.1:54145 "GET /api/shibes HTTP/1.1" 200 79 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.000000
2018-08-21T06:26:06.503930200-04:00 - 127.0.0.1:54145 "GET /api/shibes HTTP/1.1" 200 79 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.000984
2018-08-21T06:26:06.857547900-04:00 - 127.0.0.1:54145 "GET /api/shibes HTTP/1.1" 200 79 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.000000
2018-08-21T06:26:14.748779500-04:00 - 127.0.0.1:54145 "GET /api/shibes?count=0 HTTP/1.1" 200 79 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.000999
2018-08-21T06:26:16.991473-04:00 - 127.0.0.1:54145 "GET /api/shibes?count=100 HTTP/1.1" 200 474 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.001999
2018-08-21T06:26:25.368293-04:00 - 127.0.0.1:54145 "GET /api/shibes?count=10 HTTP/1.1" 200 234 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.000983
2018-08-21T06:26:30.630997200-04:00 - 127.0.0.1:54145 "GET /api/shibes?count=10&urls=false HTTP/1.1" 200 186 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.001000
2018-08-21T06:26:32.859071300-04:00 - 127.0.0.1:54145 "GET /api/shibes?count=10&urls=true HTTP/1.1" 200 213 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.000999
2018-08-21T06:26:48.117886700-04:00 - 127.0.0.1:54145 "GET /api/shibes?count=10&urls=true&httpsUrls=true HTTP/1.1" 200 234 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.001001
2018-08-21T06:26:50.792477900-04:00 - 127.0.0.1:54145 "GET /api/shibes?count=10&urls=true&httpsUrls=false HTTP/1.1" 200 209 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.000999
2018-08-21T06:26:53.003453500-04:00 - 127.0.0.1:54145 "GET /api/shibes?count=10&urls=true HTTP/1.1" 200 211 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.001000
2018-08-21T06:26:55.134541100-04:00 - 127.0.0.1:54145 "GET /api/shibes?count=10 HTTP/1.1" 200 258 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" 0.001001

+ 34
- 0
log4rs.yaml View File

@@ -0,0 +1,34 @@
# Scan this file for changes every 30 seconds
refresh_rate: 30 seconds

appenders:
# An appender named "stdout" that writes to stdout
stdout:
kind: console

# An appender named "requests" that writes to a file with a custom pattern encoder
requests:
kind: file
path: "log/requests.log"
encoder:
pattern: "{d} - {m}{n}"

# Set the default logging level to "warn" and attach the "stdout" appender to the root
root:
level: info
appenders:
- stdout

loggers:
# Raise the maximum log level for events sent to the "app::backend::db" logger to "info"
app::backend::db:
level: info
actix_web::middleware::logger:
level: info
appenders:
- requests
additive: false

shibeapp:
level: debug

+ 173
- 0
src/controller.rs View File

@@ -0,0 +1,173 @@
extern crate num;
extern crate serde_json;

use actix_web::{HttpRequest, HttpResponse};
use actix_web::dev::HttpResponseBuilder;
use actix_web::http::ContentEncoding;
use askama::Template;
use images::{Endpoints, ImageManager};
use std::error::Error;

pub struct AppParams
{
pub http_url: String,
pub https_url: String
}

impl AppParams
{
pub fn clone(&self) -> Self
{
Self
{
http_url: self.http_url.clone(),
https_url: self.https_url.clone()
}
}
}

pub struct Controller<'a>
{
endpoints: Endpoints<'a>,
settings: AppParams
}

struct ApiParams
{
count: usize,
urls: bool,
https_urls: bool
}

#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate<'a>
{
dog: &'a str
}

// gets the value of the query string as a usize, or returns the default
fn get_query_usize(req: &HttpRequest, key: &'static str, default: usize) -> usize
{
let query = req.query();
match query.get(key) {
Some(s) => s.parse::<usize>().unwrap_or(default),
None => default
}
}

fn get_query_bool(req: &HttpRequest, key: &'static str, default: bool) -> bool
{
let query = req.query();
match query.get(key) {
Some(s) => s == "true",
None => default
}
}

fn ok(content: String) -> HttpResponse
{
HttpResponse::Ok()
.content_encoding(ContentEncoding::Auto)
.content_type("application/json")
.body(content)
}

fn error(mut res : HttpResponseBuilder, message: &'static str) -> HttpResponse
{
res
.content_encoding(ContentEncoding::Auto)
.content_type("application/json")
.body(message)
}

impl<'a> Controller<'a>
{
pub fn new(
endpoints: Endpoints<'a>,
settings: AppParams)
-> Result<Self, Box<Error + 'static>>
{
Ok(Self { settings: settings, endpoints: endpoints })
}

pub fn index(&self) -> HttpResponse
{
let im = self.endpoints.get("shibes").unwrap();
let params = ApiParams { count: 1, https_urls: true, urls: true };
let dog_rand = self.get_images_for(&im, "shibes", &params);
let none = String::from("");
let index = IndexTemplate { dog: dog_rand.first().unwrap_or(&none) };
match index.render() {
Ok(html) => HttpResponse::Ok().body(html),
Err(_e) => error(HttpResponse::InternalServerError(), "Failed to render template.")
}
}

pub fn get_endpoint(&self, req: &HttpRequest) -> HttpResponse
{
let options = self.parse_api_params(req);
let endpoint = req.match_info().get("endpoint").unwrap_or("unknown");
debug!("request to endpoint {}", endpoint);

match self.endpoints.get(endpoint) {
Some(im) => self.serialize_images(&im, endpoint, &options),
None => error(HttpResponse::NotFound(), "Invalid endpoint.")
}
}

fn parse_api_params(&self, req: &HttpRequest) -> ApiParams
{
let count = num::clamp(get_query_usize(&req, "count", 1), 1, 100);
ApiParams {
count: count,
urls: get_query_bool(&req, "urls", true),
https_urls: get_query_bool(&req, "httpsUrls", true)
}
}

fn handle_url(&self, options: &ApiParams, endpoint: &str, file: &str) -> String
{
if !options.urls
{
// split on the last period and take the first item (the filename without extension)
file.splitn(2, ".").next().unwrap().to_owned()
}
else if options.https_urls
{
let mut base = self.settings.https_url.clone();
base.push_str(endpoint);
base.push('/');
base.push_str(file);
base
}
else
{
let mut base = self.settings.http_url.clone();
base.push_str(endpoint);
base.push('/');
base.push_str(file);
base
}
}

fn get_images_for(&self, im: &ImageManager, endpoint: &str, options: &ApiParams) -> Vec<String>
{
return im.get_rand_iter(options.count)
.map(|f| self.handle_url(&options, &endpoint, &f))
.collect();
}

fn serialize_images(
&self,
im: &ImageManager,
endpoint: &str,
options: &ApiParams) -> HttpResponse
{
let rand = self.get_images_for(&im, &endpoint, &options);
match serde_json::to_string(&rand) {
Ok(json) => ok(json),
Err(_e) => error(HttpResponse::InternalServerError(), "Couldn't serialize images.")
}
}
}

+ 72
- 0
src/images.rs View File

@@ -0,0 +1,72 @@
// ImageManager handles reading from directories and returning random images.

extern crate glob;
extern crate rand;

use self::glob::glob;
use rand_iter::{RandVec, RandVecIter};
use std::error::Error;
use rand::prelude::*;
use std::iter::Take;
use std::collections::HashMap;

pub struct ImageManager
{
images: RandVec<String>
}

impl ImageManager
{
// Creates a new ImageManager using files found with the given glob string.
pub fn new(glob_str: &str) -> Result<Self, Box<Error>>
{
let files = try!(glob(glob_str)).filter_map(Result::ok);
let filenames : Vec<String> = files
.filter(|p| p.is_file())
.filter_map(|p| {
let of = p.file_name();
match of {
Some(f) => Some(f.to_string_lossy().into_owned()),
None => None
}
})
.collect();

let vec = RandVec::new(filenames);

Ok(Self { images: vec })
}

pub fn get_rand_iter(&self, num: usize) -> Take<RandVecIter<String, rand::ThreadRng>>
{
self.images.rand_iter(thread_rng()).take(num)
}
}

pub struct Endpoints<'a>
{
endpoints: HashMap<&'a str, ImageManager>
}

impl<'a> Endpoints<'a>
{
pub fn new() -> Self
{
Self { endpoints: HashMap::new() }
}

pub fn add(&mut self, key: &'static str, glob: &'static str) -> Result<(), Box<Error>>
{
let im = try!(ImageManager::new(glob));
self.endpoints.insert(key, im);
Ok(())
}

pub fn get(&'a self, name: &str) -> Option<&'a ImageManager>
{
match self.endpoints.get(name) {
Some(im) => Some(&im),
None => None
}
}
}

+ 56
- 0
src/main.rs View File

@@ -0,0 +1,56 @@
extern crate actix_web;
#[macro_use] extern crate askama;
extern crate config;
#[macro_use] extern crate log;
extern crate log4rs;
extern crate rand;
mod controller;
mod images;
mod rand_iter;

use actix_web::{server, App};
use actix_web::http::Method;
use actix_web::middleware::Logger;
use config::Config;
use controller::{AppParams, Controller};
use images::Endpoints;
use std::net::SocketAddr;

fn create_app(settings : AppParams) -> App
{
let mut endpoints = Endpoints::new();
endpoints.add("shibes", "content/shibes/*").unwrap();
let controller = Controller::new(endpoints, settings).unwrap();

App::new()
.middleware(Logger::default())
.resource(
"/api/{endpoint}",
move |r| {
r.method(Method::GET).f(move |req| controller.get_endpoint(&req))
})
}

fn main()
{
log4rs::init_file("log4rs.yaml", Default::default()).unwrap();
info!("starting server");

let mut settings_file = Config::default();

settings_file
.merge(config::File::with_name("Settings")).unwrap()
.merge(config::Environment::with_prefix("SHIBE")).unwrap();

let settings = AppParams {
http_url: settings_file.get_str("base_http_url").unwrap(),
https_url: settings_file.get_str("base_https_url").unwrap(),
};

let port : u16 = settings_file.get_int("port").unwrap_or(6767) as u16;
server::new(move || create_app(settings.clone()))
.bind(SocketAddr::from(([127, 0, 0, 1], port)))
.unwrap()
.run();
}

+ 43
- 0
src/rand_iter.rs View File

@@ -0,0 +1,43 @@
// RandVecIter is an iterator that returns a random value from a RandVec.
use rand::Rng;

pub struct RandVec<T>
{
vec: Vec<T>
}

impl<T: Clone> RandVec<T>
{
pub fn new(vec: Vec<T>) -> Self
{
Self { vec: vec }
}

pub fn rand_iter<'a, R: Rng>(&'a self, rng: R) -> RandVecIter<T, R>
{
RandVecIter{ vec: self, rng: rng }
}

fn next_rand<'a, R: Rng>(&'a self, rng: &mut R) -> Option<T>
{
// generate a random index and return it
let index = rng.gen_range(0, self.vec.len());
Some(self.vec[index].clone())
}
}

pub struct RandVecIter<'a, T: Clone + 'a, R: Rng + 'a>
{
vec: &'a RandVec<T>,
rng: R
}

impl<'a, T: Clone + 'a, R: Rng> Iterator for RandVecIter<'a, T, R>
{
type Item = T;

fn next(&mut self) -> Option<T>
{
self.vec.next_rand(&mut self.rng)
}
}

+ 120
- 0
templates/index.html View File

@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html>
<head>
<title>shibe.online - the shiba inu api</title>
<link href="/css/bootstrap.min.css" rel="stylesheet" />
<link href="/css/style.css" rel="stylesheet" />
<link href="/favicon.ico?v=1.1" rel="icon" type="image/x-icon" />
<!-- oh yeah, gonna get that SEO. gonna be famous. -->
<meta property="og:title" content="shibe.online - the shiba inu api" />
<meta property="og:site_name" content="shibe.online" />
<meta property="og:url" content="http://shibe.online/" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="shibe.online - the shiba inu api" />
<meta name="twitter:description" content="shibes as a service" />
<meta name="twitter:image" content="{{ dog }}" />
<meta name="description" content="shibes as a service" />
</head>
<body>
<div class="shibe__header">
<h1>shibe.online</h1>
<h2>shibes as a service</h2>
</div>
<div class="shibe__container container">
<p>
In this day and age, everyone and their dog has an API. So why not have an API for dogs? Shibe.online is that API.
</p>
<p>
To get some dogs, just make a GET request to the following URL:
</p>
<p>
<pre>
http://shibe.online/api/shibes?count=[1-100]&urls=[true/false]&httpsUrls=[true/false]
</pre>
</p>
<p>
And you'll get back a JSON response of random images of shibes that's something like this:
</p>
<p>
<pre>
[
"image url 1",
"image url 2",
...
]
</pre>
</p>
<p>
And if those options in the URL aren't self-explanatory enough, here's a simple guide:
</p>
<p>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th width="12%">Possible Values</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>count</td>
<td>integers 1-100</td>
<td>
The number of images to be returned. Default is 1.
</td>
</tr>
<tr>
<td>urls</td>
<td>true/false</td>
<td>
Should the API return URLs or filenames without extensions? URLs are the default.
</td>
</tr>
<tr>
<td>httpsUrls</td>
<td>
true/false
</td>
<td>
Should the URLs be HTTPS or HTTP? They're HTTPS by default now.
</td>
</tr>
</tbody>
</table>
</p>
<p>
You can also get pictures of cats at /api/cats or birds at /api/birds. Same parameters apply.
</p>
<p>
That's it! Have a nice day. Here's a shibe:
</p>
<p class="shibe__image__container">
<img class="shibe__image" src="{{ dog }}" alt="A shibe." />
</p>
<p>
Service by <a href="https://cybre.space/@commie">@commie@cybre.space</a>.
</p>
<p>
Special thanks to <a href="https://ja.md/">Jammy</a> for the shibes, <a href="http://www.vision.caltech.edu/visipedia/CUB-200.html">CalTech</a> for the birds, and
<a href="https://web.archive.org/web/20150703060412/http://137.189.35.203/WebUI/CatDatabase/catData.html">Microsoft Research Asia</a> et al for the cats!
</p>
</div>

<!-- Matomo -->
<script type="text/javascript">
var _paq = _paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//the-nsa.anime.lgbt/";
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', '4']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->
</body>
</html>

Loading…
Cancel
Save