Skip to content

Instantly share code, notes, and snippets.

@blindman2k
Last active August 29, 2015 14:10
Show Gist options
  • Save blindman2k/5a416e76547e937bb9c5 to your computer and use it in GitHub Desktop.
Save blindman2k/5a416e76547e937bb9c5 to your computer and use it in GitHub Desktop.
Meeting minder using the latest Google Calendar API for PHP.
//------------------------------------------------------------------------------------------------
const CALENDAR_URL = "http://devious-dorris.gopagoda.com/meeting_minder";
const REFRESH_TIME = 60; // Once a minute
SESSION_TOKEN <- "token" in server.load() ? server.load().token : "";
// server.log(SESSION_TOKEN);
function check_calendar(reschedule = true) {
local headers = {};
headers["X-GCal-Session-Token"] <- SESSION_TOKEN;
headers["X-Imp-Agent"] <- http.agenturl();
http.get(CALENDAR_URL + "/get", headers).sendasync(function (res) {
handle_response(res.body, reschedule);
})
}
imp.wakeup(REFRESH_TIME - (time() % 60), check_calendar);
function handle_response(body, reschedule) {
local now = null;
local next = null;
try {
if (body == "") {
// Do nothing
} else if (body.find("uthenticat") != null) {
server.log("No longer authenticated. Click " + http.agenturl());
device.send("display", "AUTH")
} else {
local json = http.jsondecode(body);
if (typeof json == "array") {
device.send("display", null)
} else if (typeof json == "table") {
now = ("now" in json) ? json.now : null;
next = ("next" in json) ? json.next : null;
device.send("display", {now=now, next=next});
if ("now_desc" in json) server.log("Now: " + json.now_desc);
if ("next_desc" in json) server.log("Next: " + json.next_desc);
}
}
} catch (e) {
device.send("display", "AUTH")
if (body && body.len() > 0) server.log(body);
else server.error(e);
}
// Set the next wakeup, either after a fixed amount of time or on the next event, whichever is sooner
local next_wakeup = REFRESH_TIME - (time() % 60);
if (now != null && time() + next_wakeup > now) {
next_wakeup = now - time();
}
if (reschedule) {
imp.wakeup(next_wakeup, check_calendar);
}
}
//------------------------------------------------------------------------------------------------
device.on("command", function(command) {
server.log("Command '" + command + "' requested")
switch (command) {
case "ready":
check_calendar(false);
break;
case "end":
case "extend":
local headers = {};
headers["X-GCal-Session-Token"] <- SESSION_TOKEN;
headers["X-Imp-Agent"] <- http.agenturl();
http.get(CALENDAR_URL + "/" + command, headers).sendasync(function (res) {
if (res.statuscode == 200) {
handle_response(res.body, false);
} else {
device.send("error", command)
}
});
break;
}
})
//------------------------------------------------------------------------------------------------
http.onrequest(function(req, res) {
switch (req.path) {
case "/":
res.header("Location", CALENDAR_URL + "/login?agent=" + http.agenturl());
res.send(302, "Redirect");
break;
case "/token":
// Might be worth adding an API key here for basic security.
SESSION_TOKEN = req.body;
server.save({"token":SESSION_TOKEN})
res.send(200, "OK");
server.log(SESSION_TOKEN ? ("New token received: " + SESSION_TOKEN) : "Token cleared");
check_calendar(false);
return;
case "/sms":
// This is a Twilio HTTP GET request
res.header("Content-Type", "text/xml");
local response = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<Response>
<Message>Your meeting has been scheduled.</Message>
</Response>";
res.send(200, response);
break;
default:
res.send(404, "Not found");
return;
}
})
//------------------------------------------------------------------------------------------------
server.log("Agent booted")
// Holtek HT16K33 LED Controller Driver
// for Adafruit 7-segment i2c backpack
//------------------------------------------------------------------------------------------------
class SevenSeg {
// Pixel layout
// 0x01 = TOP CENTER
// 0x02 = TOP RIGHT
// 0x04 = BOTTOM RIGHT
// 0x08 = BOTTOM CENTER
// 0x10 = BOTTOM LEFT
// 0x20 = TOP LEFT
// 0x40 = CENTER
static colon = 0x02; // :
static blank = 0x00; // _
static number = [ 0x3F, // 0
0x06, // 1
0x5B, // 2
0x4F, // 3
0x66, // 4
0x6D, // 5
0x7D, // 6
0x07, // 7
0x7F, // 8
0x6F, // 9
0x00]; // 10 = null
static letter = { A = 0xF7,
u = 0x1C,
t = 0x78,
h = 0x74,
E = 0x79,
K = 0x76,
O = 0x3f,
R = 0x77,
r = 0x50,
"-": 0x40};
// Commands
static OSC_OFF = "\x20";
static OSC_ON = "\x21";
static DISP_OFF = "\x80";
static DISP_ON = "\x81";
static DISP_BLINK_2HZ = "\x83";
static DISP_BLINK_1HZ = "\x85";
static DISP_BLINK_05HZ = "\x87";
//Preconfigured I2C device
i2c = null;
// 8-bit base address
baseAddr = null;
// Name
name = null;
// The current display
when = null;
show_colon = true;
constructor(_i2c, _baseAddr, _name) {
this.i2c = _i2c;
this.baseAddr = _baseAddr;
this.name = _name;
write(OSC_ON);
write(DISP_ON);
setBrightness(0.6);
}
function write(str){
local result = i2c.write(baseAddr, str.tostring());
//server.log("Result ("+baseAddr+"): "+result);
}
//Float from 0 to 1.0 where 1.0 is max brightness
function setBrightness(b){
if(b < 0){ b = 0.0;}
if(b > 1){ b = 1.0;}
write( (0xE0 | (b*15.0).tointeger()).tochar() );
}
function formatTimeDiff(_time, roundup = true) {
local time = {};
time.diff <- math.abs(::time() - _time);
time.hour <- time.diff / 3600;
time.min <- ((time.diff + (roundup ? 59 : 0)) / 60) % 60; // Round up
// server.log(format("%s: The difference between now (%d) and then (%d) is %d sec (%d:%02d).", name, ::time(), _time, time.diff, time.hour, time.min))
return time;
}
function displayClear (){
//Write enough bits to clear all of memory
write("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00");
}
function displayText(msg) {
local str = blob(11);
for (local i = 0; i < msg.len() && i < 4; i++) {
local ch = msg[i];
if (ch-'0' in number) {
str.writen(0x00, 'b');
str.writen(number[ch-'0'], 'b');
// server.log("Number: " + (ch-'0'))
} else if (ch.tochar() in letter) {
str.writen(0x00, 'b');
str.writen(letter[ch.tochar()], 'b');
// server.log("Letter: " + ch.tochar())
} else {
str.writen(0x00, 'b');
str.writen(blank, 'b');
// server.log("Blank: " + ch)
}
// Blank out the colon
if (i == 1) {
str.writen(0x00, 'b');
str.writen(blank, 'b');
}
}
str.writen(0x00, 'b');
write(str);
}
function displayTime(hours, mins) {
local time = blob(11);
time.writen(0x00, 'b');
time.writen(number[(hours/10) == 0 ? 10 : (hours/10)], 'b');
time.writen(0x00, 'b');
time.writen(number[(hours%10)], 'b');
time.writen(0x00, 'b');
time.writen(show_colon ? colon : blank, 'b');
time.writen(0x00, 'b');
time.writen(number[(mins/10).tointeger()], 'b');
time.writen(0x00, 'b');
time.writen(number[(mins%10)], 'b');
time.writen(0x00, 'b');
write(time);
}
function update() {
if (typeof when == "string") {
displayText(when);
} else if (when == null || time() > when) {
displayClear();
} else {
local time = formatTimeDiff(when);
displayTime(time.hour, time.min);
}
// show_colon = !show_colon;
}
}
//------------------------------------------------------------------------------------------------
enableblinkup <- false;
busy <- false;
last_green_button <- 1;
last_red_button <- 1;
waiting_for_depress <- false;
function button_press() {
// Debounce and read
imp.sleep(0.01);
local green_button = grn_btn.read();
local red_button = red_btn.read();
// Handle both buttons being down - turn on blinkup
if (green_button == 0 && red_button == 0) {
grn.when = "----";
red.when = "----";
agent.send("command", "ready");
// server.log("Enable blinkup")
waiting_for_depress = true;
if (!enableblinkup) {
imp.enableblinkup(true);
enableblinkup = true;
imp.wakeup(600, function() {
imp.enableblinkup(false);
enableblinkup = false;
})
}
}
// Handle just the green button
else if (green_button == 1 && red_button == 1 && last_green_button == 0 && last_red_button == 1 && !waiting_for_depress && !busy) {
// server.log("Make a new 15 minute event or extend the current event by 15 minutes")
grn.when = "----";
agent.send("command", "extend");
}
// Handle just the red button
else if (green_button == 1 && red_button == 1 && last_green_button == 1 && last_red_button == 0 && !waiting_for_depress && !busy) {
// server.log("Close the current event")
grn.when = "----";
agent.send("command", "end");
}
// All the buttons are up, restart the process
else if (green_button == 1 && red_button == 1 && waiting_for_depress) {
// server.log("Buttons reset to depressed positions")
waiting_for_depress = false;
}
// Record the new value of the buttons
last_green_button <- green_button;
last_red_button <- red_button;
// Make sure we don't get more events for a bit
if (!busy) {
busy = true;
imp.wakeup(0.1, function() {
busy = false;
})
}
}
//------------------------------------------------------------------------------------------------
server.log("Device booted");
imp.enableblinkup(false);
hardware.i2c12.configure(CLOCK_SPEED_100_KHZ);
grn <- SevenSeg(hardware.i2c12, 0xE0, "Green");
red <- SevenSeg(hardware.i2c12, 0xE2, "Red");
grn_btn <- hardware.pin5;
red_btn <- hardware.pin7;
grn_btn.configure(DIGITAL_IN_PULLUP, button_press);
red_btn.configure(DIGITAL_IN_PULLUP, button_press);
//------------------------------------------------------------------------------------------------
function update() {
imp.wakeup(1, update);
grn.update();
red.update();
}
update();
//------------------------------------------------------------------------------------------------
agent.on("display", function (clocks) {
if (clocks == null) {
grn.when = null;
red.when = null;
} else if (clocks == "AUTH") {
grn.when = "Auth";
red.when = "Err";
} else {
grn.when = ("now" in clocks) ? clocks.now : null;
red.when = ("next" in clocks) ? clocks.next : null;
}
})
agent.on("error", function (command) {
if (command == "extend") {
grn.when = "ERR!";
} else if (command == "end") {
red.when = "ERR!";
}
});
agent.send("command", "ready");
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
// This is a vanilla CodeIgniter installation with the addition of the google API below.
require_once __DIR__ . '/../libraries/google-api-php-client/autoload.php';
/*
* Create a Google project (https://console.developers.google.com/project) and give it access to the Calendar API.
* Finally get a Client ID for a Web application.
* You will need to put in the correct Redirect URI's for development and production.
* eg. https://devious-dorris.gopagoda.com/meeting_minder/oauth2callback
* You will also need the correct Javascript origins.
* eg. https://devious-dorris.gopagoda.com
*/
define('G_CLIENT_ID', '');
define('G_SECRET', '');
class Meeting_Minder extends CI_Controller {
var $client = null;
var $gcal = null;
var $agenturl = null;
var $access_token = null;
//---------------------------------------------------------------------------------------------------------
function __construct() {
parent::__construct();
$this->load->library('session');
$this->client = new Google_Client();
$this->client->setApplicationName("Meeting Minder");
$this->client->setClientId(G_CLIENT_ID);
$this->client->setClientSecret(G_SECRET);
$this->client->setRedirectUri(base_url("meeting_minder/oauth2callback"));
$this->client->setAccessType('offline');
$this->client->setApprovalPrompt('force');
$this->client->setScopes(array(
// 'https://www.googleapis.com/auth/plus.me', // These three are required for getting more details about the user
// 'https://www.googleapis.com/auth/userinfo.email', // Such as their name and email address.
// 'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/calendar.readonly'
));
$this->gcal = new Google_Service_Calendar($this->client);
if (isset($_SERVER['HTTP_X_GCAL_SESSION_TOKEN'])) {
$this->access_token = $_SERVER['HTTP_X_GCAL_SESSION_TOKEN'];
} elseif ($this->session->userdata('token')) {
$this->access_token = $this->session->userdata('token');
}
if ($this->access_token) {
$this->session->set_userdata(array("token" => $this->access_token));
$this->client->setAccessToken($this->access_token);
}
if (isset($_SERVER['HTTP_X_IMP_AGENT'])) {
$this->agenturl = $_SERVER['HTTP_X_IMP_AGENT'];
} elseif ($this->input->get("agent")) {
$this->agenturl = $this->input->get("agent");
} else {
$this->agenturl = $this->session->userdata("agent");
}
if ($this->agenturl) {
$this->session->set_userdata(array("agent" => $this->agenturl));
}
// Some stuff I worked out that may come in handy one day.
// Get info on the current logged in user
// $oauth2 = new Google_Service_Oauth2($this->client);
// $userinfo = $oauth2->userinfo->get();
// Get the list of calendars
// $cals = $this->gcal->calendarList->listCalendarList();
// Get the primary calendar
// $cal = $this->gcal->calendars->get("primary");
}
//---------------------------------------------------------------------------------------------------------
public function index()
{
echo "OK";
}
//---------------------------------------------------------------------------------------------------------
public function oauth2callback()
{
if ($this->input->get('code')) {
$this->client->authenticate($this->input->get('code'));
$this->access_token = $this->client->getAccessToken();
$this->session->set_userdata(array("token" => $this->access_token));
redirect($this->session->userdata('original'));
}
}
//---------------------------------------------------------------------------------------------------------
public function login()
{
if (!$this->auth("login")) return null;
// Post the token back to the agent
if ($this->agenturl && $this->access_token) {
$result = $this->postTokenToAgent();
if ($result == TRUE) {
echo "Authorised. You can close this window.<br/>To logout click <a href='logout'>here</a>.";
} else {
header("Content-Type: text/plain");
print_r($result);
}
} else {
echo "Authorised but couldn't send to agent. You can close this window.<br/>To logout click <a href='logout'>here</a>.";
}
}
//---------------------------------------------------------------------------------------------------------
public function logout()
{
$this->session->unset_userdata('token');
$this->session->unset_userdata('agent');
if ($this->agenturl) {
$result = $this->postTokenToAgent(true);
if ($result == TRUE) {
echo "You are logged out. You can close this window.";
} else {
print_r($result);
}
} else {
echo "You are logged out but couldn't send to agent. You can close this window.";
}
}
//---------------------------------------------------------------------------------------------------------
public function get()
{
if (!$this->auth("get")) return null;
// Work out the now and next
$result = array();
try {
$nownext = $this->getNowAndNext();
if (isset($nownext['now'])) {
$result['now'] = $nownext['now']->endTime;
}
if (isset($nownext['next'])) {
$result['next'] = $nownext['next']->startTime;
}
} catch (Google_Service_Exception $e) {
$errs = $e->getErrors();
// print_r($errs[0]);
$result['exception'] = $errs[0]["message"];
}
header("Content-Type: application/json");
echo json_encode($result);
}
//---------------------------------------------------------------------------------------------------------
public function end()
{
if (!$this->auth("end")) return null;
// Work out the now and next
$result = array();
try {
$nownext = $this->getNowAndNext();
if (isset($nownext['now'])) {
// Set a new end time
$starttime = strtotime($nownext['now']->event->getStart()->getDateTime());
if ($starttime > time()-100) $result['now'] = $starttime;
else $result['now'] = time()-100;
$nownext['now']->event->getEnd()->setDateTime(gmdate("Y-m-d\TH:i:s-00:00", $result['now']));
$this->gcal->events->update("primary", $nownext['now']->event->getId(), $nownext['now']->event);
}
if (isset($nownext['next'])) {
$result['next'] = $nownext['next']->startTime;
}
} catch (Google_Service_Exception $e) {
$errs = $e->getErrors();
// print_r($errs[0]);
$result['exception'] = $errs[0]["message"];
}
header("Content-Type: application/json");
echo json_encode($result);
}
//---------------------------------------------------------------------------------------------------------
public function extend() {
if (!$this->auth("extend")) return null;
// Work out the now and next
$result = array();
try {
$nownext = $this->getNowAndNext();
$command = "none";
if (isset($nownext['now'])) {
$result['now'] = $nownext['now']->endTime + 900;
$command = "updateEventEnd";
} else {
$result['now'] = time() + 900;
$command = "createEvent";
}
// Limit the extension to the next meeting start time
if (isset($nownext['next'])) {
$result['next'] = $nownext['next']->startTime;
if ($result['now'] > $result['next']) {
$result['now'] = $result['next'];
}
}
if ($command == "updateEventEnd") {
$nownext['now']->event->getEnd()->setDateTime(gmdate("Y-m-d\TH:i:s-00:00", $result['now']));
$this->gcal->events->update("primary", $nownext['now']->event->getId(), $nownext['now']->event);
} elseif ($command == "createEvent") {
$event = new Google_Service_Calendar_Event();
$event->setSummary('Meeting added by Meeting Minder');
$start = new Google_Service_Calendar_EventDateTime();
$start->setDateTime(gmdate("Y-m-d\TH:i:s-00:00"));
$event->setStart($start);
$end = new Google_Service_Calendar_EventDateTime();
$end->setDateTime(gmdate("Y-m-d\TH:i:s-00:00", $result['now']));
$event->setEnd($end);
$this->gcal->events->insert('primary', $event);
}
if (isset($nownext['next'])) {
$result['next'] = $nownext['next']->startTime;
}
} catch (Google_Service_Exception $e) {
$errs = $e->getErrors();
$result['exception'] = $errs[0]["message"];
}
header("Content-Type: application/json");
echo json_encode($result);
}
//---------------------------------------------------------------------------------------------------------
protected function redirect_to_google($original_url)
{
$this->session->set_userdata(array("original" => base_url("meeting_minder/$original_url")));
$authUrl = $this->client->createAuthUrl();
echo "Click <a class='login' href='$authUrl'>here</a> to authenticate.";
}
//---------------------------------------------------------------------------------------------------------
protected function auth($original_url)
{
if (!$this->client->getAccessToken()) {
// We don't have an access token, go get one.
$this->redirect_to_google($original_url);
return false;
} else if ($this->client->isAccessTokenExpired()) {
// We have a token but it has expired.
if ($this->client->getRefreshToken()) {
// We have a refresh token, let it refresh
return true;
} elseif ($original_url == "login") {
// Login should always go to google, not error out.
$this->redirect_to_google($original_url);
return false;
} else {
echo "The authentication token has expired. Please login to get a new one.";
return false;
}
} else {
// All good
return true;
}
}
//---------------------------------------------------------------------------------------------------------
protected function postTokenToAgent($erase = false)
{
$ch = curl_init($this->agenturl . "/token");
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $erase ? "" : $this->access_token);
curl_setopt($ch, CURLOPT_FAILONERROR, 1);
$result = curl_exec($ch);
$info = curl_getinfo($ch);
$error = curl_error($ch);
curl_close($ch);
return $result == FALSE ? $error : TRUE;
}
//---------------------------------------------------------------------------------------------------------
protected function getNowAndNext()
{
$from = gmdate("Y-m-d\TH:i:s-00:00", strtotime("-1 day"));
$to = gmdate("Y-m-d\TH:i:s-00:00", strtotime("+1 day"));
$opts = array("orderBy" => "startTime", "singleEvents" => true, "timeMax" => $to, "timeMin" => $from);
$events = $this->gcal->events->listEvents("primary", $opts)->getItems();
$results = array();
$now = time();
foreach ($events as $event) {
$starttime = strtotime($event->getStart()->getDateTime());
$endtime = strtotime($event->getEnd()->getDateTime());
$midnight = strtotime("23:59:59");
if ($starttime <= $now && $now < $endtime) {
// We are in a meeting now
$results['now'] = (object) array("event" => $event, "startTime" => $starttime, "endTime" => $endtime);
} else if ($now < $starttime && $starttime <= $midnight) {
// We have a meeting later today
if (!isset($results['next']) || $starttime < $results['next']->startTime) {
$results['next'] = (object) array("event" => $event, "startTime" => $starttime, "endTime" => $endtime);
}
}
}
return $results;
}
}
/* End of file meeting_minder.php */
/* Location: ./application/controllers/meeting_minder.php */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment