Skip to content

Commit

Permalink
added: replace option to ms-ics-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
scolastico committed Apr 29, 2024
1 parent 0189203 commit 0817e5f
Show file tree
Hide file tree
Showing 5 changed files with 336 additions and 18 deletions.
2 changes: 1 addition & 1 deletion src/ms-ics-fix/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ COPY index.js .
COPY package.json .
COPY pnpm-lock.yaml .

RUN pnpm i
RUN pnpm i --prod

EXPOSE 3000
CMD ["node", "index.js"]
Expand Down
16 changes: 9 additions & 7 deletions src/ms-ics-fix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,17 @@ BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Microsoft Corporation//Outlook 16.0 MIMEDIR//EN
METHOD:PUBLISH
# Definition of multiple VTIMEZONE components in one file can lead to conflicts
BEGIN:VTIMEZONE
TZID:Romance Standard Time # Non-standard and ambiguous TZID
# ...
TZID:Romance Standard Time
END:VTIMEZONE
# Another timezone definition which can confuse interpretation
BEGIN:VTIMEZONE
TZID:UTC
# ...
END:VTIMEZONE
BEGIN:VEVENT
# Event uses a different timezone, not properly linked to any defined VTIMEZONE
DTSTART;TZID=Romance Standard Time:20240425T113000 # Uses TZID without proper global reference,
DTEND;TZID=Romance Standard Time:20240425T120000 # likely to fail in non-Microsoft apps
DTSTART;TZID=Central Europe Standard Time:20240425T113000 # Uses TZID without proper global reference,
DTEND;TZID=Central Europe Standard Time:20240425T120000 # likely to fail in non-Microsoft apps
# ...
END:VEVENT
END:VCALENDAR
Expand All @@ -43,7 +40,7 @@ And this is not a one time issue, see:
- [answers.microsoft.com/[...]/published-calendar-events-show-incorrect-time-when](https://answers.microsoft.com/en-us/outlook_com/forum/all/published-calendar-events-show-incorrect-time-when/c8e60444-1d02-45e1-a356-486f5a9370fc)
- [answers.microsoft.com/[...]/incorrect-timezone-when-subscribed-to-calendar](https://answers.microsoft.com/en-us/outlook_com/forum/all/incorrect-timezone-when-subscribed-to-calendar/c20444c1-df78-471d-9524-702f448c7c63)

To fix this, this tool will work like an ICS proxy, which will fix the timezone issue.
To fix this, this tool will work like an ICS proxy, which will re- parse the ICS file.

## Environment Variables

Expand All @@ -52,6 +49,8 @@ To fix this, this tool will work like an ICS proxy, which will fix the timezone
| `ICS_ON_DEMAND` | boolean | `false` | Whether to enable the on demand mode. |
| `ICS_{counter}_URL` | string | `null` | The URL of the ICS file to serve. |
| `ICS_{counter}_PATH` | string? | `null` | The path to serve the ICS file. |
| `REPLACE_{counter}_FROM` | string | `null` | The string to replace in the ICS file. |
| `REPLACE_{counter}_TO` | string | `null` | The string to replace the string from with. |

## On Demand Mode

Expand Down Expand Up @@ -79,6 +78,9 @@ services:
ICS_0_PATH: "your-new-filename-which-will-be-served.ics"
ICS_1_URL: "https://example.com/calendar2.ics"
# ...
REPLACE_0_FROM: "Romance Standard Time"
REPLACE_0_TO: "Europe/Paris"
# ...
```

This will result in to paths being published:
Expand Down
64 changes: 54 additions & 10 deletions src/ms-ics-fix/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const port = 3000
// Base config object
const config = {
onDemand: process.env.ICS_ON_DEMAND === 'true',
ics: []
ics: [],
replace: [],
}

// Parse ICS configurations from environment variables
Expand All @@ -19,6 +20,15 @@ while (process.env[`ICS_${counter}_URL`]) {
counter++
}

counter = 0
while (process.env[`REPLACE_${counter}_FROM`]) {
config.replace.push({
from: process.env[`REPLACE_${counter}_FROM`],
to: process.env[`REPLACE_${counter}_TO`],
})
counter++
}

// Validate and normalize ICS configurations
config.ics = (() => {
const n = []
Expand Down Expand Up @@ -55,12 +65,23 @@ async function cache(url, fun) {
// The little magic, microsoft is too lazy to implement...
async function doIcsFix(url) {
return await cache(url, async () => {
const raw = await fetch(url).then(res => res.text())
const raw = await (async () => {
let t = await fetch(url).then(res => res.text())
for (const r of config.replace) {
t = t.replaceAll(r.from, r.to)
}
return t
})()
const obj = await ical.async.parseICS(raw)
const events = []
for (const key in obj) {
const event = obj[key]
if (!event.uid) continue
if (event.type !== 'VEVENT') continue
if (!event.uid) {
console.warn('Event without UID:', event)
continue
}
console.log(event.summary)
const getTime = (x, t) => {
try {
const d = new Date(t)
Expand All @@ -74,15 +95,26 @@ async function doIcsFix(url) {
} catch (ignored) {}
return []
}
const getString = (x, t) => {
if (!t || t === '') return []
return [`${x}:${t.toString().replaceAll('\r\n', ' ').replaceAll('\n', ' ')}`]
}
events.push([
`BEGIN:VEVENT`,
`UID:${event.uid}`,
...getTime('DTSTART', event.start),
...getTime('DTEND', event.end),
...getTime('DTSTAMP', event.start),
`STATUS:${(event.status || '').toString().replaceAll('\n', ' ')}`,
`LOCATION:${(event.location || '').toString().replaceAll('\n', ' ')}`,
`SUMMARY:${(event.summary || '').toString().replaceAll('\n', ' ')}`,
...getString('STATUS', event.status),
...getString('SUMMARY', event.summary),
...getString('LOCATION', event.location),
...getString('DESCRIPTION', event.description),
...getString('URL', event.url),
...getString('PRIORITY', event.priority),
...getString('CLASS', event.class),
...getString('SEQUENCE', event.sequence),
...getString('TRANSP', event.transparency),
...getString('RRULE', event.rrule),
`END:VEVENT`
].join('\n'))
}
Expand All @@ -94,10 +126,16 @@ async function doIcsFix(url) {
(
raw.split('\n').find(l => l.startsWith('X-WR-CALNAME:')
) || ':Missing Calendar Name!').split(':')[1]
).replaceAll('\n', ' ')}`,
events.join('\n'),
).replaceAll('\r\n', ' ').replaceAll('\n', ' ')}`,
...events.filter(e => e),
`END:VCALENDAR`
].join('\n').replaceAll('\n', '\r\n')
]
.join('\n')
.split('\n')
.map(x => x.length > 75 ? x.match(/.{1,75}/g).join('\n ') : x)
.join('\n')
.replaceAll('\r\n', '\n')
.replaceAll('\n', '\r\n')
})
}

Expand All @@ -106,7 +144,13 @@ app.get('*', (req, res) => {
const path = req.path.substring(1)

const sendICS = (string) => {
res.set('Content-Type', 'text/calendar')
// if req params contains as-text, send as text
if (req.query['as-text']) {
res.set('Content-Type', 'text/plain')
} else {
res.set('Content-Disposition', 'attachment; filename="calendar.ics"')
res.set('Content-Type', 'text/calendar')
}
res.send(string)
}

Expand Down
6 changes: 6 additions & 0 deletions src/ms-ics-fix/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
{
"scripts": {
"dev": "ICS_ON_DEMAND=true nodemon node index.js"
},
"dependencies": {
"express": "^4.19.2",
"node-ical": "^0.18.0"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}
Loading

0 comments on commit 0817e5f

Please sign in to comment.