Got an iCal feed full of nonsense that you don't need? Does your calendar client have piss-poor filtering options (looking at your Google Calendar)?
Use ical-filter-proxy to (you guessed it) proxy and filter your iCal feed.
Define your source calendar and filtering options in config.yml and you're
good to go.
In addition, display alarms can be created or cleared.
This fork runs an unofficial, filtered Gwinnett County Public Schools calendar on Vercel. See CHANGELOG.md for releases.
| What | Where |
|---|---|
| Subscribe page (share this) | https://gcps-school-calendar.vercel.app |
| Feed | https://gcps-school-calendar.vercel.app/gwinnett-school.ics |
| Source feed | https://www.gcpsk12.org/fs/calendar-manager/events.ics?calendar_ids=1 |
| Config | gwinnett-school in config.yml |
| Subscribe page source | public/index.html |
What it keeps: school-specific dates: breaks, early release days, digital
learning days, teacher planning days, first/last day, semester start/end and
the 100th day. General holidays (Labor Day, MLK Day, Christmas Day...), board
meetings and observances are dropped, since subscribers already have those.
The filter is an allowlist on event titles, so new kinds of district noise
never get through. extra_events adds dates the district feed leaves out.
Deployment: the Vercel project gcps-school-calendar deploys every push
to master to production automatically, so test changes before merging. Other
branches get protected preview deployments.
Yearly update checklist (when the next school-year PDF is published):
- Compare the PDF against the live feed (titles and date spans).
- Add any school dates missing from the district feed to
extra_events, and remove last year's entries. - Update the year in
name, the page<title>/heading and the fixture-based spec (spec/gwinnett_config_spec.rb) with a fresh feed snapshot. - Run
bundle exec rake, add a CHANGELOG entry, bump the version, merge tomasterand tag the release.
my_calendar_name:
ical_url: https://source-calendar.com/my_calendar.ics # Source calendar
api_key: myapikey # (optional) append ?key=myapikey to your URL to grant access
name: My Calendar # (optional) calendar name shown by subscribing clients
timezone: Europe/London # (optional) ensure all time comparisons are done in this TZ
rules:
- field: start_time # start_time and end_time supported
operator: not-equals # equals and not-equals supported
val: "09:00" # A time in 24hour format, zero-padded
- field: summary # summary and description supported
operator: startswith # (not-)startswith, (not-)equals and (not-)includes supported
val: # array of values also supported
- Planning
- Daily Standup
- field: summary # summary and description supported
operator: matches # match against regex pattern
val: # array of values also supported
- '/Team A/i'
- field: blocking # blocking (TRANSP) field supported
operator: equals
val: true # true will filter out non-blocking events
alarms: # (optional) create/clear alarms for filtered events
clear_existing: true # (optional) if true, existing alarms will be removed, default: false
triggers: # (optional) triggers for new alarms. Description will be the alarm summary, action is 'DISPLAY'
- '-P1DT0H0M0S' # iso8601 supported
- 2 days # supports full day[s], hour[s], minute[s], no combination in one trigger
extra_events: # (optional) all-day events missing from the source, added after filtering
- summary: No School
date: '2027-01-04' # YYYY-MM-DD
end_date: '2027-01-05' # (optional) inclusive, defaults to date
description: Teacher planning day # (optional)It might be useful to inject configuration values as environment variable.
Variables are substituted if they begin with ICAL_FILTER_PROXY_<value> and are
defined in the configuration like ${ICAL_FILTER_PROXY_<value>}.
Example:
api_key: ${ICAL_FILTER_PROXY_API_KEY}If a placeholder is defined but environment variable is missing, it is substituted with an empty string!
At the moment rules are pretty simple, supporting only start times, end times,
equals and not-equals as that satisfies my use case. To add support for
additional rules please extend lib/ical_filter_proxy/filter_rule.rb. Pull
requests welcome.
After you've created a config.yml simply bundle and run rackup.
bundle install
bundle exec rackup -p 8000Voila! Your calendar will now be available at http://localhost:8000/my_calendar_name?key=myapikey.
I'd recommend running it behind something like nginx, but you can do what you like.
Create a config.yml as shown above.
docker build -t ical-filter-proxy .
docker run -d --name ical-filter-proxy -v $(pwd)/config.yml:/app/config.yml -p 8000:8000 ical-filter-proxyCommit your config.yml and import the repo as a Vercel project. api/index.rb
runs on Vercel's Ruby runtime, and vercel.json limits static files to public/
so the config and source aren't served. Calendars are available at
https://<project>.vercel.app/api?calendar=my_calendar_name&key=my_api_key.
Add a rewrite in vercel.json for a clean .ics URL. Note that Vercel
Authentication protects preview deployments, so share the production domain.
ical-filter-proxy can be run as an AWS Lambda process using their API Gateway.
Create a new API Gateway in the AWS Console and link to to a new Lambda process. This should create all of the permissions required in AWS land.
Next we need to package the app up ready for Lambda. First of all, craft your config.yml and place it in the root of the source directory. A handy rake task is included which will fetch any dependencies and zip them up ready to be uploaded.
bundle exec rake lamba:buildThis task will output the file ical-filter-proxy.zip. Note that you must have
the zip utility installed locally to use this task.
When prompted during the Lambda setup, provide this zip file and set the handler
to lambda.handle.
That's it! Your calendar should now be available at
https://aws-api-gateway-host/default/gateway_name?calendar=my_calendar_name&key=my_api_key