Commit ff2cd6d8 authored by Mike Jones's avatar Mike Jones 🌶

Single-shot WebSocket view

parents
config.yml
*.swp
# AleTrail
Maps of Untappd check-ins.
## Installation
... TODO ...
## License
Copyright 2018 Mike Jones
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#!/usr/bin/env perl
use utf8;
use Data::Printer;
use Furl;
use Getopt::Long 'GetOptions';
use Mojolicious::Lite;
use Mojo::Cache;
use Mojo::URL;
use Mojo::IOLoop;
use JSON::MaybeXS qw(decode_json encode_json);
use YAML::Syck 'LoadFile';
use constant {
ALL_RESULTS => 'All results retrieved',
WAIT_LEN => 5,
};
################################################################################
my $config_file = 'config.yml';
GetOptions('config_file|file=s' => \$config_file);
unless ($config_file && -e $config_file) {
die "Missing config file (${config_file})";
}
my $config = LoadFile($config_file);
my $url = sprintf(
'%s/%%s?client_id=%s&client_secret=%s&limit=50',
$config->{untappd}->{base_url},
$config->{untappd}->{client_id},
$config->{untappd}->{client_secret},
);
my $beer_cache = Mojo::Cache->new(max_keys => 50);
################################################################################
get '/' => sub {
my $c = shift;
$c->stash(access_token => $config->{map}->{token});
$c->stash(attrib => $config->{map}->{attrib});
return $c->render(template => 'index');
};
websocket '/beers/ws/:name' => sub {
my $c = shift;
my $name = $c->param('name');
my $finished = 0;
my %offset;
$c->app->log->debug('Opened WebSocket');
$c->inactivity_timeout(300);
unless ($name) {
$finished = 1;
$c->finish(4000 => 'Missing "name" parameter');
}
my $existing_beers = $beer_cache->get($name);
if ($existing_beers && scalar keys %{$existing_beers} > 0) {
$c->send($existing_beers);
$c->finish(1000 => ALL_RESULTS);
}
my %user_beers;
Mojo::IOLoop->recurring(5 => sub {
$offset{$name} ||= 0;
$c->app->log->debug(sprintf('User: %s | Offset: %d', $name, $offset{$name}));
my $uurl = sprintf($url, $name);
$uurl .= '&max_id='.$offset{$name} if $offset{$name} > 0;
my $murl = Mojo::URL->new($uurl);
my $res = Furl->new->get($murl);
my $response = decode_json($res->content);
if ($response->{meta}->{error_detail}) {
$c->app->log->warn($response->{meta}->{error_detail});
$c->finish(1013 => 'Untappd API error: '.$response->{meta}->{error_detail});
Mojo::IOLoop->stop_gracefully;
}
if ($response->{response} && $response->{response}->{checkins} && $response->{response}->{checkins}->{items}) {
my $new_beers = _format_response_as_checkin_list($response->{response}->{checkins}->{items});
foreach my $location (keys %{$new_beers}) {
foreach my $checkin (@{$new_beers->{$location}}) {
unless (grep { $_->{checkin_id} eq $checkin->{checkin_id} } @{$user_beers{$location}}) {
push @{$user_beers{$location}}, $checkin;
}
$offset{$name} = $checkin->{checkin_id};
}
}
$c->send(encode_json(\%user_beers));
}
if ($response->{response}->{checkins}->{count} < 50) {
$c->finish(1000 => ALL_RESULTS);
Mojo::IOLoop->stop_gracefully;
}
$offset{$name} += 50;
});
$c->on(finish => sub {
my ($c, $code, $reason) = @_;
$reason ||= 'No reason was provided';
$beer_cache->set($name, \%user_beers) if scalar keys %user_beers > 0;
$c->app->log->debug("WebSocket closed with status ${code} (${reason})");
});
};
################################################################################
sub _format_response_as_checkin_list {
my $checkins = shift;
my %out;
foreach (@{$checkins}) {
next unless ref $_->{venue} eq 'HASH';
next unless exists $_->{venue} && exists $_->{venue}->{location};
my $lat_lon = _format_lat_lon($_->{venue}->{location});
push @{$out{$lat_lon}}, {
abv => $_->{beer}->{beer_abv},
beer => $_->{beer}->{beer_name},
beer_label => $_->{beer}->{beer_label},
style => $_->{beer}->{beer_style},
active => $_->{brewery}->{brewery_active},
brewery => $_->{brewery}->{brewery_name},
brewery_label => $_->{brewery}->{brewery_label},
country => $_->{brewery}->{country_name},
venue_name => $_->{venue}->{venue_name},
checkin_id => $_->{checkin_id},
}
}
return \%out;
}
sub _format_lat_lon {
my $location = shift;
my @parts = ($location->{lat}, $location->{lng});
return join ':', @parts;
}
################################################################################
app->start;
__END__
map:
attrib: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>'
token: 'CHANGEME'
untappd:
base_url: 'https://api.untappd.com/v4/user/checkins/'
client_id: 'CHANGEME'
client_secret: 'CHANGEME'
requires 'Data::Printer', '0.38';
requires 'Furl', '3.11';
requires 'JSON::MaybeXS', '1.003005';
requires 'Mojolicious', '7.77';
requires 'YAML::Syck', '1.29';
requires 'Getopt::Long', '2.49';
requires 'List::Util', '1.45';
body {
padding: 0;
margin: 0;
}
div#map {
height: 100vh;
z-index: 1;
}
div#control_background, #controls {
border-radius: 2px;
bottom: 5px;
left: 5px;
float: left;
height: 150px;
padding: 10px;
position: absolute;
width: 200px;
z-index: 100;
}
div#control_background {
background: white;
opacity: 0.8;
}
button#untappd_search {
width: 100%;
}
var map = L.map('map').setView([ 51.505, -0.09 ], 13);
var markers = [];
// Set up the map
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', {
attribution: attrib,
maxZoom: 18,
id: 'mapbox.streets',
accessToken: accessToken,
}).addTo(map);
/**
* Watch for an "enter" keypress in the username form item and trigger a button
* press if it happens.
*
* @return void
*/
$('#untappd_username').on('keyup', function(event) {
if (event.keyCode === 13) $('#untappd_search').click();
});
/**
* Trigger a beer check-in search.
*
* @return void
*/
$('#untappd_search').on('click', function() {
var username = $('#untappd_username').val();
$(this).attr('disabled', 'disabled');
clear();
if (username === '') {
$(this).removeAttr('disabled');
return;
}
loadBeersWs(username);
});
/**
* Remove all the markers from the map.
*
* @return void
*/
function clear() {
for (var i = 0; i < markers.length; i++)
map.removeLayer(markers[i]);
}
/**
* Load recent check-ins and plot on the map.
*
* @param string username
* @return void
*/
function loadBeersWs(username) {
var ws = new WebSocket('ws://web01.host1.lab.netsplit.uk:3000/beers/ws/' + username);
console.log(ws);
ws.onopen = function(evt) {
console.log('Starting query for ' + username);
}
ws.onmessage = function(evt) {
var locations = $.parseJSON(evt.data);
console.log(evt);
clear();
plotBeers(locations);
};
ws.onclose = function(evt) {
if (evt.code === 1013) {
$('#controls').attr('title', evt.reason).tooltip();
$('#untappd_search')
.attr('class', 'btn btn-danger')
.text('Error!')
.attr('data-toggle', 'tooltip');
} else {
$('#untappd_search').removeAttr('disabled');
}
console.log('WebSocket Closed');
};
ws.onerror = function(evt) {
console.log(evt);
}
}
/**
* Plot check-in locations and beer consumed on the map.
*
* @return void
*/
function plotBeers(locations) {
for (var kx in locations) {
var [ lat, lon ] = kx.split(':');
var marker = L.marker([ lat, lon ]).addTo(map);
markers.push(marker);
var list = $('<div>'),
ul = $('<ul>');
$(list).append($('<h4>').html(locations[kx][0].venue_name));
for (var idx in locations[kx]) {
var checkin = locations[kx][idx];
var desc = checkin.beer + ' - ' + checkin.brewery + ' (' + checkin.abv + '%)';
$('<li />').text(desc).appendTo(ul);
}
$(list).append(ul);
marker.bindPopup(list[0].outerHTML);
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<link
rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css"
integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4"
crossorigin="anonymous"
>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css"
integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ=="
crossorigin=""
>
<link rel="stylesheet" type="text/css" href="css/style.css">
</head>
<body>
<div id="control_background"></div>
<div id="controls">
<div class="form-group">
<label for="untappd_username">Untappd username</label>
<input
name="untappd_username"
id="untappd_username"
type="text"
class="form-control"
placeholder="n7st"
>
</div>
<div class="form-group">
<button class="btn btn-primary" type="button" name="untappd_search" id="untappd_search">
Search
</button>
</div>
</div>
<div id="map"></div>
<script
src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js"
integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ"
crossorigin="anonymous"
></script>
<script
src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js"
integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm"
crossorigin="anonymous"
></script>
<script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js"
integrity="sha512-/Nsx9X4HebavoBvEBuyp3I7od5tA0UzAxs+j83KgC8PU0kgB4XiK4Lfe4y4cgBtaRJQEIFCW+oC506aPT2L1zw=="
crossorigin=""
></script>
<script type="text/javascript">
var accessToken = "<%= $access_token %>",
attrib = "<%= $attrib %>";
</script>
<script src="js/aletrail.js"></script>
</body>
</html>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment