Skip to content

Commit

Permalink
Merge pull request #710 from frog-pond/hawken/ical
Browse files Browse the repository at this point in the history
  • Loading branch information
hawkrives authored May 6, 2024
2 parents 368bb3e + 9f559b0 commit a438564
Show file tree
Hide file tree
Showing 14 changed files with 133 additions and 46 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
branches: [ "master" ]

jobs:
build:
test:
runs-on: ubuntu-latest
strategy:
matrix:
Expand All @@ -24,7 +24,8 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
- run: npm run test-stolaf-college
- run: npm run test-carleton-college

lint:
runs-on: ubuntu-latest
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"start": "node -r dotenv/config source/ccc-server/index.js",
"stolaf-college": "env INSTITUTION=stolaf-college npm start",
"carleton-college": "env INSTITUTION=carleton-college npm start",
"test": "./scripts/smoke-test.sh"
"test-stolaf-college": "./scripts/smoke-test.sh stolaf-college",
"test-carleton-college": "./scripts/smoke-test.sh stolaf-college",
"test": "npm run test-stolaf-college; npm run test-carleton-college;"
},
"dependencies": {
"@sentry/node": "^7.113.0",
Expand All @@ -28,6 +30,7 @@
"dotenv": "16.4.5",
"get-urls": "12.1.0",
"html-entities": "2.5.2",
"ical.js": "^2.0.1",
"is-absolute-url": "4.0.1",
"jsdom": "24.0.0",
"koa": "2.15.3",
Expand Down
17 changes: 14 additions & 3 deletions scripts/smoke-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
# exit the script if any command exits
set -e -o pipefail

INSTITUTION="${1:?usage: smoke-test.sh <stolaf-college|carleton-college>}"
echo "running smoke-test for $INSTITUTION"

if [[ ! $CI ]]; then
trap "exit" INT TERM
trap "kill 0" EXIT
fi

#for institution in stolaf-college carleton-college; do
# check that the server can launch properly, but don't bind to a port
env SMOKE_TEST=1 npm run stolaf-college
env SMOKE_TEST=1 npm run "$INSTITUTION"

# launch and background the server, so we can test it
PORT=3000
env NODE_PORT=$PORT npm run stolaf-college &
env NODE_PORT=$PORT npm run "$INSTITUTION" &

# wait while the server starts up
until nc -z -w5 localhost $PORT; do
Expand All @@ -33,6 +35,10 @@ for route in $(curl -s localhost:3000/v1/routes | jq -r '.[].path'); do
echo "validating $route"

case $route in
"/v1/calendar/carleton" | "/v1/calendar/the-cave")
# we can run these, because they're ICS, not GCal
;;

"/v1/calendar/"* | "/v1/convos/upcoming")
echo "skip because we don't have authorization during smoke tests"
continue
Expand All @@ -48,6 +54,11 @@ for route in $(curl -s localhost:3000/v1/routes | jq -r '.[].path'); do
continue
;;

"/v1/orgs")
echo "skip because presence is so slow"
continue
;;

*)
# do nothing
;;
Expand Down
13 changes: 6 additions & 7 deletions source/calendar-google/index.js → source/calendar/google.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ import {get} from '../ccc-lib/http.js'
import moment from 'moment'
import getUrls from 'get-urls'
import {JSDOM} from 'jsdom'
import {Event} from './types.js'

function convertGoogleEvents(data, now = moment()) {
let events = data.map((event) => {
return data.map((event) => {
const startTime = moment(event.start.date || event.start.dateTime)
const endTime = moment(event.end.date || event.end.dateTime)
let description = (event.description || '').replace('<br>', '\n')
description = JSDOM.fragment(description).textContent.trim()

return {
return Event.parse({
dataSource: 'google',
startTime,
endTime,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
title: event.summary || '',
description: description,
location: event.location || '',
Expand All @@ -24,10 +25,8 @@ function convertGoogleEvents(data, now = moment()) {
endTime: true,
subtitle: 'location',
},
}
})
})

return events
}

export async function googleCalendar(calendarId, now = moment()) {
Expand Down
58 changes: 58 additions & 0 deletions source/calendar/ical.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {get} from '../ccc-lib/http.js'
import moment from 'moment'
import getUrls from 'get-urls'
import {JSDOM} from 'jsdom'
import InternetCalendar from 'ical.js'
import {Event} from './types.js'
import lodash from 'lodash'

const {sortBy} = lodash

/**
* @param {InternetCalendar.Event[]} data
* @param {typeof moment} now
* @returns {Event[]}
*/
function convertEvents(data, now = moment()) {
return data.map((event) => {
const startTime = moment(event.startDate.toString())
const endTime = moment(event.endDate.toString())
let description = JSDOM.fragment(event.description || '').textContent.trim()

return Event.parse({
dataSource: 'ical',
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
title: event.summary,
description: description,
location: event.location,
isOngoing: startTime.isBefore(now, 'day'),
links: [...getUrls(description)],
metadata: {
uid: event.uid,
},
config: {
startTime: true,
endTime: true,
subtitle: 'location',
},
})
})
}

export async function ical(url, {onlyFuture = true} = {}, now = moment()) {
let body = await get(url).text()

let comp = InternetCalendar.Component.fromString(body)
let events = comp
.getAllSubcomponents('vevent')
.map((vevent) => new InternetCalendar.Event(vevent))

if (onlyFuture) {
events = events.filter((event) =>
moment(event.endDate.toString()).isAfter(now, 'day'),
)
}

return sortBy(convertEvents(events, now), (event) => event.startTime)
}
6 changes: 4 additions & 2 deletions source/calendar-reason/index.js → source/calendar/reason.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import moment from 'moment-timezone'
import lodash from 'lodash'
import getUrls from 'get-urls'
import {JSDOM} from 'jsdom'
import {Event} from './types.js'

const {dropWhile, dropRightWhile, sortBy} = lodash

const TZ = 'US/Central'
Expand Down Expand Up @@ -116,7 +118,7 @@ function convertReasonEvent(event, now = moment()) {

let links = description ? [...getUrls(description)] : []

return {
return Event.parse({
dataSource: 'reason',
startTime: event.startTime,
endTime: event.endTime,
Expand All @@ -133,7 +135,7 @@ function convertReasonEvent(event, now = moment()) {
endTime: true,
subtitle: 'location',
},
}
})
}

export async function reasonCalendar(calendarUrl, now = moment()) {
Expand Down
19 changes: 19 additions & 0 deletions source/calendar/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {z} from 'zod'

const EventConfig = z.object({
startTime: z.boolean(),
endTime: z.boolean(),
subtitle: z.union([z.literal('location')]),
})

export const Event = z.object({
dataSource: z.string(),
startTime: z.string().datetime(),
endTime: z.string().datetime(),
title: z.string(),
description: z.string(),
isOngoing: z.boolean(),
links: z.array(z.unknown()),
config: EventConfig,
metadata: z.optional(z.unknown()),
})
32 changes: 13 additions & 19 deletions source/ccci-carleton-college/v1/calendar.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {googleCalendar} from '../../calendar-google/index.js'
import {reasonCalendar} from '../../calendar-reason/index.js'
import {googleCalendar} from '../../calendar/google.js'
import {ical} from '../../calendar/ical.js'
import {ONE_MINUTE} from '../../ccc-lib/constants.js'
import mem from 'memoize'

export const getGoogleCalendar = mem(googleCalendar, {maxAge: ONE_MINUTE})
export const getReasonCalendar = mem(reasonCalendar, {maxAge: ONE_MINUTE})
export const getInternetCalendar = mem(ical, {maxAge: ONE_MINUTE})

export async function google(ctx) {
ctx.cacheControl(ONE_MINUTE)
Expand All @@ -13,33 +13,27 @@ export async function google(ctx) {
ctx.body = await getGoogleCalendar(calendarId)
}

export async function reason(ctx) {
export async function ics(ctx) {
ctx.cacheControl(ONE_MINUTE)

let {url: calendarUrl} = ctx.query
ctx.body = await getReasonCalendar(calendarUrl)
}

export function ics(ctx) {
ctx.cacheControl(ONE_MINUTE)

ctx.throw(501, 'ICS support is not implemented yet.')
ctx.body = await getInternetCalendar(calendarUrl)
}

export async function carleton(ctx) {
ctx.cacheControl(ONE_MINUTE)

let url =
'webcal://www.carleton.edu/calendar/?loadFeed=calendar&stamp=1714843628'
ctx.body = await getGoogleCalendar(url)
'https://www.carleton.edu/calendar/?loadFeed=calendar&stamp=1714843628'
ctx.body = await getInternetCalendar(url)
}

export async function cave(ctx) {
ctx.cacheControl(ONE_MINUTE)

let url =
'webcal://www.carleton.edu/student/orgs/cave/calendar/?loadFeed=calendar&stamp=1714844429\n'
ctx.body = await getGoogleCalendar(url)
'https://www.carleton.edu/student/orgs/cave/calendar/?loadFeed=calendar&stamp=1714844429\n'
ctx.body = await getInternetCalendar(url)
}

export async function stolaf(ctx) {
Expand Down Expand Up @@ -74,14 +68,14 @@ export async function convos(ctx) {
ctx.cacheControl(ONE_MINUTE)

let url =
'webcal://www.carleton.edu/convocations/calendar/?loadFeed=calendar&stamp=1714843936'
ctx.body = await getGoogleCalendar(url)
'https://www.carleton.edu/convocations/calendar/?loadFeed=calendar&stamp=1714843936'
ctx.body = await getInternetCalendar(url)
}

export async function sumo(ctx) {
ctx.cacheControl(ONE_MINUTE)

let url =
'webcal://www.carleton.edu/student/orgs/sumo/schedule/?loadFeed=calendar&stamp=1714840383'
ctx.body = await getGoogleCalendar(url)
'https://www.carleton.edu/student/orgs/sumo/schedule/?loadFeed=calendar&stamp=1714840383'
ctx.body = await getInternetCalendar(url)
}
1 change: 0 additions & 1 deletion source/ccci-carleton-college/v1/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ api.get('/food/named/menu/schulze', menus.schulzeMenu)

// calendar
api.get('/calendar/google', calendar.google)
api.get('/calendar/reason', calendar.reason)
api.get('/calendar/ics', calendar.ics)
api.get('/calendar/named/carleton', calendar.carleton)
api.get('/calendar/named/the-cave', calendar.cave)
Expand Down
14 changes: 5 additions & 9 deletions source/ccci-stolaf-college/v1/calendar.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {googleCalendar} from '../../calendar-google/index.js'
import {reasonCalendar} from '../../calendar-reason/index.js'
import {googleCalendar} from '../../calendar/google.js'
import {ical} from '../../calendar/ical.js'
import {ONE_MINUTE} from '../../ccc-lib/constants.js'
import mem from 'memoize'

export const getGoogleCalendar = mem(googleCalendar, {maxAge: ONE_MINUTE})
export const getReasonCalendar = mem(reasonCalendar, {maxAge: ONE_MINUTE})
export const getInternetCalendar = mem(ical, {maxAge: ONE_MINUTE})

export async function google(ctx) {
ctx.cacheControl(ONE_MINUTE)
Expand All @@ -13,15 +13,11 @@ export async function google(ctx) {
ctx.body = await getGoogleCalendar(calendarId)
}

export async function reason(ctx) {
export async function ics(ctx) {
ctx.cacheControl(ONE_MINUTE)

let {url: calendarUrl} = ctx.query
ctx.body = await getReasonCalendar(calendarUrl)
}

export function ics(ctx) {
ctx.throw(501, 'ICS support is not implemented yet.')
ctx.body = await getInternetCalendar(calendarUrl)
}

export async function stolaf(ctx) {
Expand Down
1 change: 0 additions & 1 deletion source/ccci-stolaf-college/v1/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ api.get('/food/named/menu/schulze', menus.schulzeMenu)

// calendar
api.get('/calendar/google', calendar.google)
api.get('/calendar/reason', calendar.reason)
api.get('/calendar/ics', calendar.ics)
api.get('/calendar/named/stolaf', calendar.stolaf)
api.get('/calendar/named/oleville', calendar.oleville)
Expand Down
2 changes: 1 addition & 1 deletion source/ccci-stolaf-college/v1/orgs.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {ONE_HOUR} from '../../ccc-lib/constants.js'
import mem from 'memoize'

import {presence as _presence} from '../../calendar-presence/index.js'
import {presence as _presence} from '../../student-orgs/presence.js'

const CACHE_DURATION = ONE_HOUR * 36

Expand Down
File renamed without changes.

0 comments on commit a438564

Please sign in to comment.