Compare commits

77 Commits

Author SHA1 Message Date
6480b56875 Merge fixes and new dev tools
All checks were successful
Build SilverBullet Plug / build (push) Successful in 18s
2026-02-17 08:36:22 -08:00
d28c206862 Bump version to 0.2.16 and add detailed offset logging 2026-02-17 08:36:18 -08:00
GitHub Action
2ea763e145 Build and update icalendar.plug.js [skip ci] 2026-02-17 16:27:06 +00:00
66f60bc9ae Bump version to 0.2.14 and simplify date processing to native browser behavior
All checks were successful
Build SilverBullet Plug / build (push) Successful in 26s
2026-02-17 08:26:32 -08:00
GitHub Action
0e7e89091d Build and update icalendar.plug.js [skip ci] 2026-02-17 16:22:05 +00:00
81d5e8738e Bump version to 0.2.13 and improve date processing stability
All checks were successful
Build SilverBullet Plug / build (push) Successful in 23s
2026-02-17 08:21:34 -08:00
GitHub Action
899ee62693 Build and update icalendar.plug.js [skip ci] 2026-02-17 16:16:34 +00:00
90f317be6e Bump version to 0.2.12 and fix Outlook timezone logic
All checks were successful
Build SilverBullet Plug / build (push) Successful in 24s
2026-02-17 08:16:06 -08:00
GitHub Action
b50cded6c9 Build and update icalendar.plug.js [skip ci] 2026-02-17 16:10:40 +00:00
124a780b65 Bump version to 0.2.11 and add tzShift support
All checks were successful
Build SilverBullet Plug / build (push) Successful in 21s
2026-02-17 08:10:13 -08:00
GitHub Action
415cd7e215 Build and update icalendar.plug.js [skip ci] 2026-02-17 16:06:15 +00:00
9e54f0320e Bump version to 0.2.10 and add explicit date localization with logs
All checks were successful
Build SilverBullet Plug / build (push) Successful in 20s
2026-02-17 08:05:52 -08:00
GitHub Action
c422f0fae7 Build and update icalendar.plug.js [skip ci] 2026-02-17 15:59:25 +00:00
ab0db17a47 Bump version to 0.2.9 and improve ISO localization logic
All checks were successful
Build SilverBullet Plug / build (push) Successful in 28s
2026-02-17 07:58:53 -08:00
GitHub Action
8087031220 Build and update icalendar.plug.js [skip ci] 2026-02-17 15:46:04 +00:00
56b6e7d0bf Bump version to 0.2.8 and add targeted logging for problematic UID
All checks were successful
Build SilverBullet Plug / build (push) Successful in 20s
2026-02-17 07:45:37 -08:00
GitHub Action
2131bf4051 Build and update icalendar.plug.js [skip ci] 2026-02-17 15:35:46 +00:00
cdfea5f3b2 Bump version to 0.2.7 and add PST localization logging
All checks were successful
Build SilverBullet Plug / build (push) Successful in 27s
2026-02-17 07:35:13 -08:00
GitHub Action
3cc449a7c6 Build and update icalendar.plug.js [skip ci] 2026-02-17 15:29:24 +00:00
80cd15c1b5 Bump version to 0.2.6
All checks were successful
Build SilverBullet Plug / build (push) Successful in 25s
2026-02-17 07:28:57 -08:00
GitHub Action
3b348d8257 Build and update icalendar.plug.js [skip ci] 2026-02-17 15:26:36 +00:00
adf638379d Add raw ICS logging
All checks were successful
Build SilverBullet Plug / build (push) Successful in 19s
2026-02-17 07:26:10 -08:00
GitHub Action
4b4aacbfd9 Build and update icalendar.plug.js [skip ci] 2026-02-17 15:16:03 +00:00
45ab0e8d95 Bump version to 0.2.5 and add detailed date conversion logging
All checks were successful
Build SilverBullet Plug / build (push) Successful in 14s
2026-02-17 07:15:45 -08:00
GitHub Action
e79349d7c0 Build and update icalendar.plug.js [skip ci] 2026-02-17 14:52:28 +00:00
86824991a6 Bump version to 0.2.4 and add timezone diagnostics
All checks were successful
Build SilverBullet Plug / build (push) Successful in 21s
2026-02-17 06:52:03 -08:00
e3fcf743f8 Update version to 0.2.3 in PLUG.md
All checks were successful
Build SilverBullet Plug / build (push) Successful in 14s
2026-02-16 11:23:48 -08:00
GitHub Action
da835727d4 Build and update icalendar.plug.js [skip ci] 2026-02-16 18:18:44 +00:00
dbffe7fb24 Bump version to 0.2.3
All checks were successful
Build SilverBullet Plug / build (push) Successful in 20s
2026-02-16 10:18:13 -08:00
5a7a7aaa18 Robust whitespace handling in URL with character code logging
Some checks failed
Build SilverBullet Plug / build (push) Has been cancelled
2026-02-16 10:17:45 -08:00
GitHub Action
7aba023818 Build and update icalendar.plug.js [skip ci] 2026-02-16 17:39:02 +00:00
c382ab93ab Add top-level log for debugging boot timeout
All checks were successful
Build SilverBullet Plug / build (push) Successful in 20s
2026-02-16 09:38:38 -08:00
4d9943ed72 Revert to relative path in PLUG.md
All checks were successful
Build SilverBullet Plug / build (push) Successful in 22s
2026-02-16 09:36:54 -08:00
10286625cc Use full URL for JS file in PLUG.md
All checks were successful
Build SilverBullet Plug / build (push) Successful in 19s
2026-02-16 09:35:32 -08:00
7031d15833 Add version to PLUG.md
All checks were successful
Build SilverBullet Plug / build (push) Successful in 18s
2026-02-16 09:32:29 -08:00
ab303c694e Update PLUG.md name for Gitea
All checks were successful
Build SilverBullet Plug / build (push) Successful in 21s
2026-02-16 09:31:16 -08:00
31fdf3f42b Correct PLUG.md format based on template
All checks were successful
Build SilverBullet Plug / build (push) Successful in 28s
2026-02-16 08:23:07 -08:00
cb4f2c03c0 Switch PLUG.md back to table format for better compatibility
All checks were successful
Build SilverBullet Plug / build (push) Successful in 31s
2026-02-16 08:22:19 -08:00
GitHub Action
74177dc4b5 Build and update icalendar.plug.js [skip ci] 2026-02-16 16:10:53 +00:00
f2fedb690c Bump version to 0.2.2
All checks were successful
Build SilverBullet Plug / build (push) Successful in 26s
2026-02-16 08:10:15 -08:00
099374e878 Remove artifact upload step
All checks were successful
Build SilverBullet Plug / build (push) Successful in 21s
2026-02-16 08:06:49 -08:00
GitHub Action
479c096587 Build and update icalendar.plug.js [skip ci] 2026-02-16 16:05:47 +00:00
57cb085982 Add contents:write permission to fix CI push
Some checks failed
Build SilverBullet Plug / build (push) Failing after 28s
2026-02-16 08:05:17 -08:00
f847ad53bc Update SilverBullet dependency to v2.4.1
Some checks failed
Build SilverBullet Plug / build (push) Failing after 23s
2026-02-16 07:57:23 -08:00
6a862a5563 Use GITHUB_TOKEN for push in CI
Some checks failed
Build SilverBullet Plug / build (push) Failing after 27s
2026-02-16 07:50:27 -08:00
3fa0bd553b Robust fetch with User-Agent and URL encoding
Some checks failed
Build SilverBullet Plug / build (push) Failing after 26s
2026-02-16 07:48:32 -08:00
af12466721 Use edge compiler URL
Some checks failed
Build SilverBullet Plug / build (push) Failing after 28s
2026-02-16 07:43:22 -08:00
17ba5aa701 Add git diagnostics to workflow
Some checks failed
Build SilverBullet Plug / build (push) Failing after 13s
2026-02-16 07:42:23 -08:00
b8497c09d3 Fix checkout fetch-depth for CI push
Some checks failed
Build SilverBullet Plug / build (push) Failing after 14s
2026-02-16 07:41:31 -08:00
0a58c16705 Add URL trimming and fetching logs
Some checks failed
Build SilverBullet Plug / build (push) Failing after 10s
2026-02-16 07:40:35 -08:00
b59aabd115 Update to SB v0.10.4
Some checks failed
Build SilverBullet Plug / build (push) Failing after 7s
2026-02-15 17:53:21 -08:00
c39b869795 Update dependencies and add diagnostics
Some checks failed
Build SilverBullet Plug / build (push) Failing after 7s
2026-02-15 17:52:33 -08:00
e33be08320 Use raw github URL for plug-compiler
Some checks failed
Build SilverBullet Plug / build (push) Failing after 7s
2026-02-15 17:51:37 -08:00
1ce9011d60 Try building with --no-check and Deno v2.x
Some checks failed
Build SilverBullet Plug / build (push) Failing after 7s
2026-02-15 17:50:52 -08:00
56e11f748b Use stable plug-compiler v0.10.1 to fix CI
Some checks failed
Build SilverBullet Plug / build (push) Failing after 8s
2026-02-15 17:50:09 -08:00
bb1b9a93ad Make configuration more robust for single sources
Some checks failed
Build SilverBullet Plug / build (push) Failing after 9s
2026-02-15 17:39:32 -08:00
6641f03519 Update PLUG.md
Some checks failed
Build SilverBullet Plug / build (push) Failing after 9s
2026-02-16 00:05:47 +00:00
44079d525a Use Deno v1.x and add debugging to workflow
Some checks failed
Build SilverBullet Plug / build (push) Failing after 10s
2026-02-15 15:56:10 -08:00
GitHub Action
a09bfd805a Build and update icalendar.plug.js [skip ci] 2026-02-15 23:55:00 +00:00
19826c1678 Automate committing compiled plug back to repo
Some checks failed
Build SilverBullet Plug / build (push) Failing after 14s
2026-02-15 15:54:42 -08:00
651a1107d1 Align project structure with silverbullet-plug-template
Some checks failed
Build SilverBullet Plug / build (push) Failing after 11s
- Rename deno.jsonc to deno.json and update build tasks
- Add PLUG.md for SB v2 installation
- Update .gitignore to include .plug.js files
- Update README.md with new installation instructions
- Simplify GitHub workflow
2026-02-15 15:27:51 -08:00
daab3cf2f3 Update .github/workflows/publish.yml
Some checks failed
Build SilverBullet Plug / build (push) Failing after 9s
2026-02-15 15:02:07 +00:00
5ba0445eeb Update .github/workflows/publish.yml
Some checks failed
Build SilverBullet Plug / build (push) Failing after 10s
2026-02-15 15:00:21 +00:00
31fddc1e26 Update .github/workflows/publish.yml
Some checks failed
Build SilverBullet Plug / build (push) Failing after 9s
2026-02-15 14:58:47 +00:00
7ff19185e2 Update .github/workflows/publish.yml
Some checks failed
Build SilverBullet Plug / build (push) Failing after 8s
2026-02-15 14:57:29 +00:00
606340058e Update .github/workflows/publish.yml
Some checks failed
Build SilverBullet Plug / build (push) Failing after 24s
2026-02-15 14:54:48 +00:00
1107571bf1 Update .github/workflows/publish.yml
Some checks failed
Publish / publish (push) Failing after 1m6s
2026-02-15 14:49:38 +00:00
1d2fd52715 Add .github/workflows/publish.yml 2026-02-15 14:47:33 +00:00
Alexandre Nicolaie
deb30ab6b3 Migrate to ts-ics 2.4.0 API and fix duplicate recurring events
ts-ics 2.4.0 changed API from parseIcsCalendar to convertIcsCalendar
and VCalendar to IcsCalendar. The new API returns Date objects and
nested date structures that require recursive conversion to strings
for SilverBullet indexing.

Recurring events were creating duplicate refs because the hash only
used the UID, which is identical across occurrences. Including the
start date in the unique key ensures each occurrence gets a distinct
ref.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexandre Nicolaie <xunleii@users.noreply.github.com>
2025-10-18 16:15:46 +02:00
Alexandre Nicolaie
904c1b9d94 Add clear all events functionality
Add 'iCalendar: Clear All Events' command to completely remove
all indexed calendar events and cache. Useful for maintenance
and troubleshooting.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Alexandre Nicolaie <xunleii@users.noreply.github.com>
2025-10-18 16:15:46 +02:00
Alexandre Nicolaie
34bbe69569 Add automatic calendar sync on editor initialization
Calendars now sync automatically when the editor starts, eliminating the
need for manual sync after opening SilverBullet.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexandre Nicolaie <xunleii@users.noreply.github.com>
2025-10-18 16:15:46 +02:00
Alexandre Nicolaie
38dd97c25c Add force sync command
Add 'iCalendar: Force Sync' command to bypass cache and
immediately synchronize calendar events. Useful when you
need fresh data before the cache expires.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Alexandre Nicolaie <xunleii@users.noreply.github.com>
2025-10-18 16:15:46 +02:00
Alexandre Nicolaie
d3e4fc021b Migrate to SilverBullet v2 indexing system
Replace deprecated query provider with index-based architecture.
Events are now indexed using index.indexObjects() and queryable
via Lua Integrated Query (LIQ).

Breaking changes:
- Plugin now requires SilverBullet v2 (use v0.1.0 for SB v1)
- Old query syntax no longer works (use LIQ instead)
- Manual sync required via 'iCalendar: Sync' command
- Events cached for 6h by default (was real-time)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Alexandre Nicolaie <xunleii@users.noreply.github.com>
2025-10-18 16:15:46 +02:00
Marek S. Lukasiewicz
8a7c9700ee Update README 2025-01-05 19:22:02 +01:00
Marek S. Lukasiewicz
e12420aba3 Add Nextcloud screenshot 2025-01-05 19:09:48 +01:00
Marek S. Łukasiewicz
4df5a1f8a8 Update README.md 2025-01-05 18:20:40 +01:00
Marek S. Lukasiewicz
e13e6e2bc2 Add LICENSE 2025-01-05 17:59:16 +01:00
14 changed files with 418 additions and 166 deletions

40
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Build SilverBullet Plug
on:
push:
branches: [ main ]
workflow_dispatch:
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Build Plug
run: |
deno task build -- --no-check
- name: Commit and push changes
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add icalendar.plug.js
if git diff --quiet --staged; then
echo "No changes to commit"
else
git commit -m "Build and update icalendar.plug.js [skip ci]"
git push origin main
fi

1
.gitignore vendored
View File

@@ -1,4 +1,3 @@
deno.lock
*.plug.js
test_space
.env

View File

@@ -1,5 +1,5 @@
{
"deno.enable": true,
"editor.formatOnSave": true,
"deno.config": "deno.jsonc"
"deno.config": "deno.json"
}

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2025 Marek S. Lukasiewicz
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

34
Makefile Normal file
View File

@@ -0,0 +1,34 @@
.PHONY: build up down logs clean
# Build the plug using a Docker container with Deno
build:
docker run --rm -v $(PWD):/app -w /app denoland/deno:latest task build
# Start the SilverBullet test container
up:
mkdir -p test_space
docker compose up -d
# Stop the SilverBullet test container
down:
docker compose down
# View logs from the SilverBullet container
logs:
docker compose logs -f
# Watch for changes and rebuild automatically using Deno's internal watch
watch:
@echo "Starting watch in background..."
docker run -d --name ical-watch -v $(PWD):/app -w /app denoland/deno:latest task watch
@echo "Watching... Use 'docker logs -f ical-watch' to see build progress."
# Stop the watch container
stop-watch:
docker rm -f ical-watch
# Clean up build artifacts and test space
clean:
rm -f *.plug.js
# Be careful with test_space if you have notes there you want to keep
# rm -rf test_space

8
PLUG.md Normal file
View File

@@ -0,0 +1,8 @@
---
name: Library/sstent/icalendar/PLUG
version: 0.2.14
tags: meta/library
files:
- icalendar.plug.js
---
iCalendar sync plug for SilverBullet.

View File

@@ -3,19 +3,21 @@
`silverbullet-icalendar` is a [Plug](https://silverbullet.md/Plugs) for [SilverBullet](https://silverbullet.md/) which I made for my girlfriend.
It reads external [iCalendar](https://en.wikipedia.org/wiki/ICalendar) data, also known as iCal and `.ics` format, used in CalDAV protocol.
**Note**: This version (0.2.0+) is compatible with **SilverBullet v2 only**. For SilverBullet v1, use version 0.1.0.
## Installation
Run the {[Plugs: Add]} command in SilverBullet and add paste this URI into the dialog box:
Run the {[Library: Install]} command and paste the following URL:
`https://github.com/Maarrk/silverbullet-icalendar/blob/main/PLUG.md`
```
ghr:Maarrk/silverbullet-icalendar
```
Alternatively, you can use the older way with the {[Plugs: Add]} command:
`ghr:Maarrk/silverbullet-icalendar`
Then run the {[Plugs: Update]} command and off you go!
### Configuration
This plug can be configured with [Space Config](https://silverbullet.md/Space%20Config), these are the default values and their usage:
This plug is configured with [Space Config](https://silverbullet.md/Space%20Config), short example:
```yaml
icalendar:
@@ -37,57 +39,72 @@ Instructions to get the source URL for some calendar services:
- Calendar settings (pencil icon to the right of the name)
- Settings and Sharing, scroll down to Integrate calendar
- Copy the link for Secret address in iCal format
![Screenshot of getting the URL from Nextcloud Calendar](./url-nextcloud.png)
## Usage
The plug provides the query source `ical-event`, which corresponds to `VEVENT` object
After configuration, run the `{[iCalendar: Sync]}` command to synchronize calendar events. The plug will cache the results for 6 hours by default (configurable via `cacheDuration` in config).
To bypass the cache and force an immediate sync, use the `{[iCalendar: Force Sync]}` command.
To completely clear all indexed events and cache (useful for troubleshooting), use the `{[iCalendar: Clear All Events]}` command.
Events are indexed with the tag `ical-event` and can be queried using Lua Integrated Query (LIQ).
### Examples
Select events that start on a given date
Select events that start on a given date:
~~~
```query
ical-event
where start =~ /^2024-01-04/
select summary, description
```md
${query[[
from index.tag "ical-event"
where start:startsWith "2024-01-04"
select {summary=summary, description=description}
]]}
```
~~~
Get the next 5 upcoming events:
```md
${query[[
from index.tag "ical-event"
where start > os.date("%Y-%m-%d")
order by start
limit 5
]]}
```
~~~
## Roadmap
- Cache the calendar according to `REFRESH-INTERVAL` or `X-PUBLISHED-TTL`, command for manual update
- More query sources:
- Cache the calendar according to `REFRESH-INTERVAL` or `X-PUBLISHED-TTL`
- More indexed object types:
- `ical-todo` for `VTODO` components
- `ical-calendar` showing information about configured calendars
- Describe the properties of query results
- Support `file://` URL scheme (use an external script or filesystem instead of authentication on CalDAV)
## Contributing
Pull requests with short instructions for various calendar services are welcome.
If you find bugs, report them on the [issue tracker on GitHub](https://github.com/Maarrk/silverbullet-icalendar/issues).
### Building from source
To build this plug, make sure you have [SilverBullet installed](https://silverbullet.md/Install). Then, build the plug with:
To build this plug, you need [Deno](https://deno.land/) installed. Then, build the plug with:
```shell
deno task build
```
Or to watch for changes and rebuild automatically
Or to watch for changes and rebuild automatically:
```shell
deno task watch
```
Then, copy the resulting `.plug.js` file into your space's `_plug` folder. Or build and copy in one command:
```shell
deno task build && cp *.plug.js /my/space/_plug/
```
SilverBullet will automatically sync and load the new version of the plug (or speed up this process by running the {[Sync: Now]} command).
The compiled plug will be written to `icalendar.plug.js`. This file is tracked by Git in this repository to allow for easy installation via the `PLUG.md` file.
## License

25
deno.json Normal file
View File

@@ -0,0 +1,25 @@
{
"tasks": {
"build": "deno run -A https://github.com/silverbulletmd/silverbullet/releases/download/edge/plug-compile.js -c deno.json icalendar.plug.yaml",
"watch": "deno run -A https://github.com/silverbulletmd/silverbullet/releases/download/edge/plug-compile.js -c deno.json icalendar.plug.yaml -w",
"debug": "deno run -A https://github.com/silverbulletmd/silverbullet/releases/download/edge/plug-compile.js -c deno.json icalendar.plug.yaml --debug"
},
"lint": {
"rules": {
"exclude": [
"no-explicit-any"
]
}
},
"fmt": {
"exclude": [
"*.md",
"**/*.md",
"*.plug.js"
]
},
"imports": {
"@silverbulletmd/silverbullet": "jsr:@silverbulletmd/silverbullet@^2.4.1",
"ts-ics": "npm:ts-ics@2.4.0"
}
}

View File

@@ -1,22 +0,0 @@
{
"tasks": {
"build": "silverbullet plug:compile -c deno.jsonc icalendar.plug.yaml",
"watch": "silverbullet plug:compile -c deno.jsonc icalendar.plug.yaml -w"
},
"lint": {
"rules": {
"exclude": ["no-explicit-any"]
}
},
"fmt": {
"exclude": [
"*.md",
"**/*.md",
"*.plug.js"
]
},
"imports": {
"@silverbulletmd/silverbullet": "jsr:@silverbulletmd/silverbullet@^0.10.1",
"ts-ics": "npm:ts-ics@1.6.5"
}
}

10
docker-compose.yml Normal file
View File

@@ -0,0 +1,10 @@
services:
silverbullet:
image: zefhemel/silverbullet:latest
ports:
- "3000:3000"
volumes:
- ./test_space:/space
- ./icalendar.plug.js:/space/_plug/icalendar.plug.js:ro
environment:
- SB_USER=admin:admin # Default for easy local testing

10
icalendar.plug.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -2,15 +2,28 @@ name: icalendar
requiredPermissions:
- fetch
functions:
syncCalendars:
path: ./icalendar.ts:syncCalendars
command:
name: "iCalendar: Sync"
priority: -1
events:
- editor:init
forceSync:
path: ./icalendar.ts:forceSync
command:
name: "iCalendar: Force Sync"
priority: -1
clearCache:
path: ./icalendar.ts:clearCache
command:
name: "iCalendar: Clear All Events"
priority: -1
showVersion:
path: ./icalendar.ts:showVersion
command:
name: "iCalendar: Version"
priority: -2
queryEvents:
path: ./icalendar.ts:queryEvents
events:
- query:ical-event
config:
schema.config.properties.icalendar:
type: object
@@ -29,3 +42,6 @@ config:
type: string
name:
type: string
cacheDuration:
type: number
description: "Interval between two calendar synchronizations (default: 21600 = 6 hours)"

View File

@@ -1,127 +1,235 @@
import { editor, system } from "@silverbulletmd/silverbullet/syscalls";
import { QueryProviderEvent } from "@silverbulletmd/silverbullet/types";
import { applyQuery } from "@silverbulletmd/silverbullet/lib/query";
import { parseIcsCalendar, type VCalendar } from "ts-ics";
import { clientStore, config, datastore, editor, index } from "@silverbulletmd/silverbullet/syscalls";
import { convertIcsCalendar, type IcsCalendar, type IcsEvent, type IcsDateObjects } from "ts-ics";
const VERSION = "0.1.0";
const VERSION = "0.2.15";
const CACHE_KEY = "icalendar:lastSync";
const DEFAULT_CACHE_DURATION_SECONDS = 21600; // 6 hours
// Try to match SilverBullet properties where possible.
// Timestamps should be strings formatted with `localDateString`
interface Event {
// Typically available in calendar apps
summary: string | undefined;
description: string | undefined;
location: string | undefined;
// Mapping of common Windows/Outlook timezones to their standard offsets (in hours)
const TIMEZONE_OFFSETS: Record<string, number> = {
"GMT Standard Time": 0,
"W. Europe Standard Time": 1,
"Central Europe Standard Time": 1,
"Romance Standard Time": 1,
"Central European Standard Time": 1,
"Eastern Standard Time": -5,
"Central Standard Time": -6,
"Mountain Standard Time": -7,
"Pacific Standard Time": -8,
"UTC": 0,
"None": 0
};
// Same as SilverBullet pages
created: string | undefined;
lastModified: string | undefined;
// Keep consistent with dates above
start: string | undefined;
end: string | undefined;
console.log(`[iCalendar] Plug loading (Version ${VERSION})...`);
// ============================================================================
// Types
// ============================================================================
type DateToString<T> = T extends Date ? string
: T extends IcsDateObjects ? string
: T extends object ? { [K in keyof T]: DateToString<T[K]> }
: T extends Array<infer U> ? Array<DateToString<U>>
: T;
interface Source {
url: string;
name: string | undefined;
}
interface PlugConfig {
sources: Source[];
cacheDuration: number | undefined;
tzShift: number | undefined;
}
interface CalendarEvent extends DateToString<IcsEvent> {
ref: string;
tag: "ical-event";
sourceName: string | undefined;
}
interface Source {
url: string; // Should be an .ics file
name: string | undefined; // Optional name that will be assigned to events
}
// ============================================================================
// Utility Functions
// ============================================================================
export async function queryEvents(
{ query }: QueryProviderEvent,
): Promise<any[]> {
const events: Event[] = [];
const sources = await getSources();
for (const source of sources) {
const identifier = (source.name === undefined || source.name === "")
? source.url
: source.name;
try {
const result = await fetch(source.url);
const icsData = await result.text();
const calendarParsed: VCalendar = parseIcsCalendar(icsData);
if (calendarParsed.events === undefined) {
throw new Error("Didn't parse events from ics data");
}
// The order here is the default order of columns without the select clause
for (const icsEvent of calendarParsed.events) {
events.push({
summary: icsEvent.summary,
sourceName: source.name,
location: icsEvent.location,
description: icsEvent.description,
start: localDateString(icsEvent.start.date),
end: icsEvent.end ? localDateString(icsEvent.end.date) : undefined,
created: icsEvent.created
? localDateString(icsEvent.created.date)
: undefined,
lastModified: icsEvent.lastModified
? localDateString(icsEvent.lastModified.date)
: undefined,
});
}
} catch (err) {
console.error(
`Getting events from ${identifier} failed with:`,
err,
);
}
}
return applyQuery(query, events, {}, {});
}
async function getSources(): Promise<Source[]> {
const config = await system.getSpaceConfig("icalendar", {});
if (!config.sources || !Array.isArray(config.sources)) {
// The queries are running on server, probably because of that, can't use editor.flashNotification
console.error("Configure icalendar.sources");
return [];
}
const sources = config.sources;
if (sources.length === 0) {
console.error("Empty icalendar.sources");
return [];
}
const validated: Source[] = [];
for (const src of sources) {
if (typeof src.url !== "string") {
console.error(
`Invalid iCalendar source`,
src,
);
continue;
}
validated.push({
url: src.url,
name: (typeof src.name === "string") ? src.name : undefined,
});
}
return validated;
}
// Copied from @silverbulletmd/silverbullet/lib/dates.ts which is not exported in the package
export function localDateString(d: Date): string {
/**
* Standard SilverBullet local date string formatter
*/
function toLocalISO(d: Date): string {
const pad = (n: number) => String(n).padStart(2, "0");
return d.getFullYear() +
"-" + String(d.getMonth() + 1).padStart(2, "0") +
"-" + String(d.getDate()).padStart(2, "0") +
"T" + String(d.getHours()).padStart(2, "0") +
":" + String(d.getMinutes()).padStart(2, "0") +
":" + String(d.getSeconds()).padStart(2, "0") +
"-" + pad(d.getMonth() + 1) +
"-" + pad(d.getDate()) +
"T" + pad(d.getHours()) +
":" + pad(d.getMinutes()) +
":" + pad(d.getSeconds()) +
"." + String(d.getMilliseconds()).padStart(3, "0");
}
export async function showVersion() {
await editor.flashNotification(`iCalendar Plug ${VERSION}`);
/**
* Robustly converts an ICS date object to a localized PST string
*/
function processIcsDate(obj: any, manualShift = 0): string {
if (!obj) return "";
// 1. Get the "Wall Time" (the hour shown in the organizer's calendar)
// ts-ics often puts this in obj.local.date but marks it with 'Z'
let wallTimeStr = (obj.local && typeof obj.local.date === "string")
? obj.local.date
: (typeof obj.date === "string" ? obj.date : "");
if (!wallTimeStr) return "";
// Remove any 'Z' to treat it as a raw floating time initially
wallTimeStr = wallTimeStr.replace("Z", "");
// Parse as UTC so we have a stable starting point
const baseDate = new Date(wallTimeStr + "Z");
// 2. Identify the Source Timezone
const tzName = obj.local?.timezone || obj.timezone || "UTC";
const sourceOffset = TIMEZONE_OFFSETS[tzName] ?? 0;
// 3. Calculate True UTC
// UTC = WallTime - SourceOffset
// Example: 16:00 WallTime in GMT+1 (+1) -> 15:00 UTC
const utcMillis = baseDate.getTime() - (sourceOffset * 3600000);
const envOffset = new Date().getTimezoneOffset();
console.log(`[iCalendar] Date Calc: Wall=${wallTimeStr}, TZ=${tzName}, SourceOffset=${sourceOffset}, EnvOffset=${envOffset}, UTC=${new Date(utcMillis).toISOString()}`);
// 4. Apply User's Manual Shift (if any)
const finalMillis = utcMillis + (manualShift * 3600000);
// 5. Localize to environment
return toLocalISO(new Date(finalMillis));
}
function isIcsDateObjects(obj: any): obj is IcsDateObjects {
return obj && typeof obj === 'object' && ('date' in obj && 'type' in obj);
}
async function sha256Hash(str: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
}
function convertDatesToStrings<T>(obj: T, hourShift = 0): DateToString<T> {
if (obj === null || obj === undefined) return obj as DateToString<T>;
if (isIcsDateObjects(obj)) {
return processIcsDate(obj, hourShift) as DateToString<T>;
}
if (obj instanceof Date) {
return toLocalISO(new Date(obj.getTime() + (hourShift * 3600000))) as DateToString<T>;
}
if (Array.isArray(obj)) {
return obj.map(item => convertDatesToStrings(item, hourShift)) as DateToString<T>;
}
if (typeof obj === 'object') {
const result: any = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result[key] = convertDatesToStrings((obj as any)[key], hourShift);
}
}
return result as DateToString<T>;
}
return obj as DateToString<T>;
}
// ============================================================================
// Configuration & Commands
// ============================================================================
async function getSources(): Promise<Source[]> {
const plugConfig = await config.get<PlugConfig>("icalendar", { sources: [] });
if (!plugConfig.sources) return [];
let sources = plugConfig.sources;
if (!Array.isArray(sources)) sources = [sources as unknown as Source];
return sources.filter(s => typeof s.url === "string");
}
async function fetchAndParseCalendar(source: Source, hourShift = 0): Promise<CalendarEvent[]> {
let url = source.url.trim();
if (url.includes(" ")) url = encodeURI(url);
const response = await fetch(url, {
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" }
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const icsData = await response.text();
const calendar: IcsCalendar = convertIcsCalendar(undefined, icsData);
if (!calendar.events) return [];
return await Promise.all(calendar.events.map(async (icsEvent: IcsEvent): Promise<CalendarEvent> => {
const uniqueKey = `${icsEvent.start?.date || ''}${icsEvent.uid || icsEvent.summary || ''}`;
const ref = await sha256Hash(uniqueKey);
return convertDatesToStrings({
...icsEvent,
ref,
tag: "ical-event" as const,
sourceName: source.name,
}, hourShift);
}));
}
export async function syncCalendars() {
try {
const plugConfig = await config.get<PlugConfig>("icalendar", { sources: [] });
const hourShift = plugConfig.tzShift ?? 0;
const sources = await getSources();
if (sources.length === 0) return;
await editor.flashNotification("Syncing calendars...", "info");
const allEvents: CalendarEvent[] = [];
for (const source of sources) {
try {
const events = await fetchAndParseCalendar(source, hourShift);
allEvents.push(...events);
} catch (err) {
console.error(`[iCalendar] Failed to sync ${source.name}:`, err);
}
}
await index.indexObjects("$icalendar", allEvents);
await editor.flashNotification(`Synced ${allEvents.length} events`, "info");
} catch (err) {
console.error("[iCalendar] Sync failed:", err);
}
}
export async function forceSync() {
await clientStore.del(CACHE_KEY);
await syncCalendars();
}
export async function clearCache() {
if (!await editor.confirm("Clear all calendar events?")) return;
const pageKeys = await datastore.query({ prefix: ["ridx", "$icalendar"] });
const allKeys: any[] = [];
for (const { key } of pageKeys) {
allKeys.push(key);
allKeys.push(["idx", ...key.slice(2), "$icalendar"]);
}
if (allKeys.length > 0) await datastore.batchDel(allKeys);
await clientStore.del(CACHE_KEY);
await editor.flashNotification("Calendar index cleared", "info");
}
export async function showVersion() {
await editor.flashNotification(`iCalendar Plug ${VERSION}`, "info");
}

BIN
url-nextcloud.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB