Syncing iCal/ICS feeds into Discourse topics (simple Python script, cron-friendly)

I recorded a run-through of setting up the ICS → Discourse sync on a fresh DigitalOcean droplet.
This reply shares a timeline of what I did, with clickable YouTube timestamps so you can jump straight to the relevant parts of the video:

:play_button: Full video here


Timestamp Description/Command Issued
0:12 DO Droplet image selected as Ubuntu 24.04 (LTS), not 25.04 :slight_smile:
1:33 ufw enabled with my usual configuration
2:47 Discourse main repository with standalone.yml cloned to DO droplet
3:01 Created an A record from namecheap to ipv4 of DO droplet
3:35 A record didn’t propagate in time, so ./discourse-setup connection to the allowed https port fails
4:03 Manually change DISCOURSE_HOSTNAME in app.yml, forgot to uncomment 2 Let’s Encrypt lines
4:28 Changed SMTP setting for dummy, might not be necessary?
4:55 ./launcher rebuild app starts
9:46 ./launcher rebuild app finishes
10:18 ran rake admin:create because didn’t bother setting up SMTP
12:57 arrived in discourse but without https
13:32 calendar_enabled admin site setting
13:39 added general as a calendar category
14:03 Discourse post event enabled. Next time, change max tag length from 20 to 30 aswell
14:15 was made aware that general has category id 4
15:33 apt install -y python3 python3-venv python3-pip curl nano ca-certificates
16:46 mkdir -p /opt/ics_sync
16:58 chown $USER:$USER /opt/ics_sync
17:08 cd /opt/ics_sync
17:19 python3 -m venv venv
17:35 source venv/bin/activate
17:47 pip install --upgrade pip
17:59 pip install requests python-dateutil icalendar
18:33 cat > /opt/ics_sync/.env <<'EOF'
18:51 export DISCOURSE_BASE_URL="https://your.forum.url"
19:18 export DISCOURSE_API_KEY="YOUR_DISCOURSE_API_KEY"
19:27 setup API key for system user
19:34 make it granular, should have also ticked “tags → list”
20:38 ran command with relevant API key
20:43 export DISCOURSE_API_USERNAME="system"
20:51 export ICS_SOURCE="https://example.com/feed.ics"
21:21 export DISCOURSE_CATEGORY_ID=4
21:23 export SITE_TZ="Europe/London"
21:27 export DEFAULT_TAGS="events,ics"
(missing) export ICS_NAMESPACE="uon-mycal" however its import is missing on the script
21:38 EOF
22:00 cd /opt/ics_sync
22:07 set -a
22:12 source .env
22:17 set +a
22:25 nano /opt/ics_sync/ics_to_discourse.py
22:47 chmod +x /opt/ics_sync/ics_to_discourse.py
curl -sS -X POST "$DISCOURSE_BASE_URL/posts.json" \
  -H "Api-Key: $DISCOURSE_API_KEY" \
  -H "Api-Username: $DISCOURSE_API_USERNAME" \
  -F "title=API test $(date +%s)" \
  -F 'raw=[event start="2025-10-10T10:00:00Z" end="2025-10-10T11:00:00Z" timezone="Europe/London"]Test[/event]' \
  -F "category=$DISCOURSE_CATEGORY_ID"

If https is working on your Discourse, the above API test works fine.


Timestamp Description/Command Issued
24:19 crontab -e
25:00 1
(cron job added)
26:35 check the status of cron in systemctl
27:18 Destroy DO Droplet

Conclusion:
This exercise shows that the script works end-to-end with a clean Ubuntu 24.04 droplet.
I still need to add support for ICS_NAMESPACE in the script (to avoid tag collisions across feeds), but otherwise the setup went smoothly.

Big thanks to everyone who contributed improvements on this thread — hopefully the timestamps + video help others get it running more quickly.

3 Likes