Created
October 14, 2019 13:43
-
-
Save wilcoschoneveld/163bc3442e33c977a4f14b4a7fd2d46f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { google } from 'googleapis'; | |
import { Injectable, HttpService } from '@nestjs/common'; | |
import { InjectSchedule, Schedule } from 'nest-schedule'; | |
import { LoggerService } from '../logger/logger.service'; | |
interface GoogleCalendarEvent { | |
id: string; | |
summary: string; | |
description: string; | |
// ... | |
// more properties can be found here: | |
// https://googleapis.dev/nodejs/googleapis/latest/calendar/interfaces/Schema$Event.html | |
} | |
/* | |
* CalendarService for polling a Google Calendar and looking for scheduled events to broadcast | |
* in a slack channel at the scheduled time. For each event, a message can be set explicitely. | |
* | |
* - A calendar must be selected with an id specified in the CALENDAR_ID environment variable | |
* - A service account json must be available through a location set in GOOGLE_APPLICATION_CREDENTIALS | |
* - Event summary (title) must be in the format of '[#channel] ...', e.g. '[#general] simple reminder' | |
* - The broadcasted message must be placed in the event description | |
* - A processed event summary will be set to '[!channel]' (or '[?channel]' if no message was found) | |
* | |
* The Google NodeJS API can be found here: | |
* https://github.com/googleapis/google-api-nodejs-client | |
* | |
* The Google calendar API and resources can be found here: | |
* https://googleapis.dev/nodejs/googleapis/latest/calendar/index.html | |
*/ | |
@Injectable() | |
export class CalendarService { | |
calendar: ReturnType<typeof google.calendar>; | |
POLLING_INTERVAL = 5 * 1000; // in milliseconds | |
constructor(private loggerService: LoggerService, | |
private httpService: HttpService, | |
@InjectSchedule() private readonly schedule: Schedule) { | |
// Connecting to google calendar is asynchronous, fired from constructor | |
this.connectToCalendar(); | |
} | |
async connectToCalendar() { | |
try { | |
// Create a new GoogleAuth instance with service-account credentials from a json file. | |
// Note: credential file location must be set in GOOGLE_APPLICATION_CREDENTIALS env variable | |
const auth = new google.auth.GoogleAuth({ | |
scopes: ['https://www.googleapis.com/auth/calendar'], | |
clientOptions: { | |
// Overwrite JWT subject to 'impersonate' calendar user | |
subject: '[email protected]' | |
} | |
}); | |
this.calendar = google.calendar({ | |
version: 'v3', | |
auth: await auth.getClient() | |
}); | |
} catch (err) { | |
this.loggerService.log(`[calendar] could not connect to calendar API: ${err}`); | |
return; | |
} | |
this.loggerService.log('[calendar] successfully connected to calendar API'); | |
if (!process.env.EVENT_CALENDAR_ID) { | |
this.loggerService.log('[calendar] no CALENDAR_ID found in env, poller not started'); | |
return; | |
} | |
this.loggerService.log('[calendar] starting poller'); | |
this.schedule.scheduleIntervalJob('poller', this.POLLING_INTERVAL, () => this.pollCalendarEvents()); | |
} | |
async pollCalendarEvents() { | |
this.loggerService.debug('[calendar] polling messages...'); | |
// Define a range of twice the polling interval (in case of polling delays) | |
const timeNow = new Date(); | |
const timeMin = new Date(timeNow.getTime() - this.POLLING_INTERVAL * 2); | |
try { | |
// Request all events in the selected calendar in the selected range | |
const response = await this.calendar.events.list({ | |
calendarId: process.env.EVENT_CALENDAR_ID, | |
singleEvents: true, // Flatten any recurring events | |
timeMin: timeMin.toISOString(), | |
timeMax: timeNow.toISOString() | |
}); | |
const events = response.data.items as GoogleCalendarEvent[]; | |
// Process events one by one (sequentially) | |
for (const event of events) { | |
this.loggerService.debug(`[calendar] found event: ${event.summary}`); | |
await this.processEvent(event); | |
} | |
} catch (err) { | |
this.loggerService.log(`[calendar] poller failed: ${err}`) | |
} | |
// Keep the interval job running | |
return false; | |
} | |
async processEvent(event: GoogleCalendarEvent) { | |
// Test event for a summary in the format of '[#channel] ...' | |
const regex = /^\[(#[a-z0-9-_]+)\]/; | |
const elements = regex.exec(event.summary); | |
// This event does not match format (could have been processed before) | |
if (elements === null) { | |
return; | |
} | |
this.loggerService.log(`[calendar] processing event: ${event.summary}`) | |
// Channel is extracted from regex, message is in event description | |
const channel = elements[1]; // first captured group | |
const message = event.description; | |
if (message) { | |
await this.postSlackMessage(channel, message); | |
} | |
// Set new summary to '[!channel] ...' (or '[?channel] ...' if no message was found) | |
const newSymbol = message ? '!' : '?'; | |
const newSummary = '[' + newSymbol + event.summary.substring(2); | |
// Patch calendar event with new summary | |
await this.calendar.events.patch({ | |
calendarId: process.env.EVENT_CALENDAR_ID, | |
eventId: event.id, | |
requestBody: { | |
summary: newSummary | |
} | |
}); | |
} | |
async postSlackMessage(channel: string, text: string) { | |
const data = { channel, text, link_names: true }; | |
const config = { headers: { Authorization: 'Bearer ' + process.env.SLACK_TOKEN }}; | |
const endpoint = 'https://slack.com/api/chat.postMessage'; | |
await this.httpService.post(endpoint, data, config).toPromise(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment