Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ After these initial setup steps, you're ready to create an OAuth 2.0 client ID:

6. **Authenticate with Google:**
- Inside the MMM-GoogleCalendar directory, run `node authorize.js` from your terminal.
- This command will open a Google sign-in page in your web browser. Log in with your Google account as you normally would.
- This command will open a Google sign-in page in your web browser. Log in with your Google account as you normally would. If your browser does not open, use the link from the output in your terminal.
- During this process, you might see a screen alerting you that "Google hasn't verified this app." This is a standard message for apps using OAuth that aren't published yet. Simply look for and click on the "Continue" button to proceed with the authentication.

By completing these steps, you've successfully laid the groundwork for your Google Calendar to communicate with your MagicMirror. The module is installed, and with the necessary permissions configured, you're ready to personalize your calendar settings.
Expand Down
178 changes: 97 additions & 81 deletions authorize.js
Original file line number Diff line number Diff line change
@@ -1,94 +1,110 @@
const fs = require("fs").promises;
const path = require("path");
const process = require("process");
const { authenticate } = require("@google-cloud/local-auth");
const { google } = require("googleapis");
const fs = require('fs');
const readline = require('readline');
const { google } = require('googleapis');
const http = require('http');
const destroyer = require('server-destroy');

// If modifying these scopes, delete token.json.
const SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = path.join(process.cwd(), "token.json");
const CREDENTIALS_PATH = path.join(process.cwd(), "credentials.json");
console.log('Starting authorization process...');

/**
* Reads previously authorized credentials from the save file.
*
* @return {Promise<OAuth2Client|null>}
*/
async function loadSavedCredentialsIfExist() {
try {
const content = await fs.readFile(TOKEN_PATH);
const credentials = JSON.parse(content);
return google.auth.fromJSON(credentials);
} catch (err) {
console.error("MMM-GoogleCalendar: Error loading credentials", err);
return null;
const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'];
const TOKEN_PATH = 'token.json';

process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
});

process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});

// Load client secrets from a local file.
fs.readFile('credentials.json', (err, content) => {
if (err) {
console.log('Error loading client secret file:', err);
return;
}
}
console.log('Successfully loaded credentials.json');
authorize(JSON.parse(content), listEvents);
});

/**
* Serializes credentials to a file compatible with GoogleAUth.fromJSON.
*
* @param {OAuth2Client} client
* @return {Promise<void>}
*/
async function saveCredentials(client) {
const content = await fs.readFile(CREDENTIALS_PATH);
const keys = JSON.parse(content);
const key = keys.installed || keys.web;
const payload = JSON.stringify({
type: "authorized_user",
client_id: key.client_id,
client_secret: key.client_secret,
refresh_token: client.credentials.refresh_token
// Authorize function
function authorize(credentials, callback) {
console.log('Authorizing with provided credentials...');
const { client_secret, client_id, redirect_uris = [] } = credentials.installed || credentials.web || {};
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);

console.log('Checking for existing token...');
fs.readFile(TOKEN_PATH, (err, token) => {
if (err) {
console.log('No existing token found, requesting new access token...');
return getAccessToken(oAuth2Client, callback, redirect_uris); // Fetch new token if not available
}
console.log('Token found, proceeding...');
oAuth2Client.setCredentials(JSON.parse(token));
callback(oAuth2Client);
});
await fs.writeFile(TOKEN_PATH, payload);
}

/**
* Load or request or authorization to call APIs.
*
*/
async function authorize() {
let client = await loadSavedCredentialsIfExist();
if (client) {
return client;
}
client = await authenticate({
scopes: SCOPES,
keyfilePath: CREDENTIALS_PATH
// Get Access Token function
function getAccessToken(oAuth2Client, callback, redirect_uris) {
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
if (client.credentials) {
await saveCredentials(client);
}
return client;
}

/**
* Lists the next 10 events on the user's primary calendar.
* @param {google.auth.OAuth2} auth An authorized OAuth2 client.
*/
async function listEvents(auth) {
const calendar = google.calendar({ version: "v3", auth });
const res = await calendar.events.list({
calendarId: "primary",
timeMin: new Date().toISOString(),
maxResults: 10,
singleEvents: true,
orderBy: "startTime"
console.log('Generated auth URL:', authUrl);

// Desktop app flow (localhost)
const server = http.createServer(async (req, res) => {
console.log(`Incoming request: ${req.method} ${req.url}`); // Log incoming requests
try {
if (req.url.startsWith('/')) { // Accept any path
console.log(`Handling callback for: ${req.url}`);
const qs = new URL(req.url, `http://localhost:${req.socket.localPort}`).searchParams;
res.end('Authentication successful! Please return to the console.');
server.destroy();
const { tokens } = await oAuth2Client.getToken(qs.get('code'));
oAuth2Client.setCredentials(tokens);
fs.writeFile(TOKEN_PATH, JSON.stringify(tokens), (err) => {
if (err) console.error(err);
console.log('Token stored to', TOKEN_PATH);
});
callback(oAuth2Client);
}
} catch (e) {
console.error(e);
}
});
const events = res.data.items;
if (!events || events.length === 0) {
console.log("MMM-GoogleCalendar: No upcoming events found.");
return;
}
console.log("MMM-GoogleCalendar: Upcoming 10 events:");
events.map((event, i) => {
const start = event.start.dateTime || event.start.date;
console.log(`${start} - ${event.summary}`);

server.on('error', (err) => {
console.error('Server error:', err);
});

server.listen(80, () => {
console.log('Server listening at http://localhost');
console.log('Opening browser for authorization:', authUrl);
import('open').then((open) => {
open.default(authUrl);
console.log(`If the browser does not open, visit this URL: ${authUrl}`);
});
});
destroyer(server);
}

authorize().then(listEvents).catch(console.error);
// List events to confirm authorization
function listEvents(auth) {
const calendar = google.calendar({ version: 'v3', auth });
calendar.calendarList.list({}, (err, res) => {
if (err) {
console.log('The API returned an error: ' + err);
return;
}
const calendars = res.data.items;
if (calendars.length) {
console.log('Your calendars:');
calendars.forEach((cal) => console.log(`- ${cal.summary} (ID: ${cal.id})`));
} else {
console.log('No calendars found.');
}
});
}
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mmm-googlecalendar",
"version": "1.1.4",
"version": "1.1.5",
"displayName": "MMM-GoogleCalendar",
"description": "Display your Google calendars in MagicMirror (including google's family calendar) without using iCals nor any public calendar. ",
"main": "MMM-GoogleCalendar.js",
Expand All @@ -27,6 +27,9 @@
"homepage": "https://github.com/randomBrainstormer/MMM-GoogleCalendar#readme",
"dependencies": {
"@google-cloud/local-auth": "^2.1.0",
"googleapis": "^105.0.0"
"googleapis": "^105.0.0",
"open": "^10.1.0",
"readline": "^1.3.0",
"server-destroy": "^1.0.1"
}
}