forked from GitHubMirrors/silverbullet-icalendar
Compare commits
75 Commits
v0.1.0
...
2ea763e145
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ea763e145 | ||
| 66f60bc9ae | |||
|
|
0e7e89091d | ||
| 81d5e8738e | |||
|
|
899ee62693 | ||
| 90f317be6e | |||
|
|
b50cded6c9 | ||
| 124a780b65 | |||
|
|
415cd7e215 | ||
| 9e54f0320e | |||
|
|
c422f0fae7 | ||
| ab0db17a47 | |||
|
|
8087031220 | ||
| 56b6e7d0bf | |||
|
|
2131bf4051 | ||
| cdfea5f3b2 | |||
|
|
3cc449a7c6 | ||
| 80cd15c1b5 | |||
|
|
3b348d8257 | ||
| adf638379d | |||
|
|
4b4aacbfd9 | ||
| 45ab0e8d95 | |||
|
|
e79349d7c0 | ||
| 86824991a6 | |||
| e3fcf743f8 | |||
|
|
da835727d4 | ||
| dbffe7fb24 | |||
| 5a7a7aaa18 | |||
|
|
7aba023818 | ||
| c382ab93ab | |||
| 4d9943ed72 | |||
| 10286625cc | |||
| 7031d15833 | |||
| ab303c694e | |||
| 31fdf3f42b | |||
| cb4f2c03c0 | |||
|
|
74177dc4b5 | ||
| f2fedb690c | |||
| 099374e878 | |||
|
|
479c096587 | ||
| 57cb085982 | |||
| f847ad53bc | |||
| 6a862a5563 | |||
| 3fa0bd553b | |||
| af12466721 | |||
| 17ba5aa701 | |||
| b8497c09d3 | |||
| 0a58c16705 | |||
| b59aabd115 | |||
| c39b869795 | |||
| e33be08320 | |||
| 1ce9011d60 | |||
| 56e11f748b | |||
| bb1b9a93ad | |||
| 6641f03519 | |||
| 44079d525a | |||
|
|
a09bfd805a | ||
| 19826c1678 | |||
| 651a1107d1 | |||
| daab3cf2f3 | |||
| 5ba0445eeb | |||
| 31fddc1e26 | |||
| 7ff19185e2 | |||
| 606340058e | |||
| 1107571bf1 | |||
| 1d2fd52715 | |||
|
|
deb30ab6b3 | ||
|
|
904c1b9d94 | ||
|
|
34bbe69569 | ||
|
|
38dd97c25c | ||
|
|
d3e4fc021b | ||
|
|
8a7c9700ee | ||
|
|
e12420aba3 | ||
|
|
4df5a1f8a8 | ||
|
|
e13e6e2bc2 |
40
.github/workflows/publish.yml
vendored
Normal file
40
.github/workflows/publish.yml
vendored
Normal 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
1
.gitignore
vendored
@@ -1,4 +1,3 @@
|
|||||||
deno.lock
|
deno.lock
|
||||||
*.plug.js
|
|
||||||
test_space
|
test_space
|
||||||
.env
|
.env
|
||||||
|
|||||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"deno.enable": true,
|
"deno.enable": true,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"deno.config": "deno.jsonc"
|
"deno.config": "deno.json"
|
||||||
}
|
}
|
||||||
|
|||||||
7
LICENSE
Normal file
7
LICENSE
Normal 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.
|
||||||
8
PLUG.md
Normal file
8
PLUG.md
Normal 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.
|
||||||
63
README.md
63
README.md
@@ -3,19 +3,21 @@
|
|||||||
`silverbullet-icalendar` is a [Plug](https://silverbullet.md/Plugs) for [SilverBullet](https://silverbullet.md/) which I made for my girlfriend.
|
`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.
|
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
|
## 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`
|
||||||
|
|
||||||
```
|
Alternatively, you can use the older way with the {[Plugs: Add]} command:
|
||||||
ghr:Maarrk/silverbullet-icalendar
|
`ghr:Maarrk/silverbullet-icalendar`
|
||||||
```
|
|
||||||
|
|
||||||
Then run the {[Plugs: Update]} command and off you go!
|
Then run the {[Plugs: Update]} command and off you go!
|
||||||
|
|
||||||
### Configuration
|
### 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
|
```yaml
|
||||||
icalendar:
|
icalendar:
|
||||||
@@ -38,56 +40,71 @@ Instructions to get the source URL for some calendar services:
|
|||||||
- Settings and Sharing, scroll down to Integrate calendar
|
- Settings and Sharing, scroll down to Integrate calendar
|
||||||
- Copy the link for Secret address in iCal format
|
- Copy the link for Secret address in iCal format
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Usage
|
## 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
|
### Examples
|
||||||
|
|
||||||
Select events that start on a given date
|
Select events that start on a given date:
|
||||||
|
|
||||||
~~~
|
~~~
|
||||||
```query
|
```md
|
||||||
ical-event
|
${query[[
|
||||||
where start =~ /^2024-01-04/
|
from index.tag "ical-event"
|
||||||
select summary, description
|
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
|
## Roadmap
|
||||||
|
|
||||||
- Cache the calendar according to `REFRESH-INTERVAL` or `X-PUBLISHED-TTL`, command for manual update
|
- Cache the calendar according to `REFRESH-INTERVAL` or `X-PUBLISHED-TTL`
|
||||||
- More query sources:
|
- More indexed object types:
|
||||||
- `ical-todo` for `VTODO` components
|
- `ical-todo` for `VTODO` components
|
||||||
- `ical-calendar` showing information about configured calendars
|
- `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)
|
- Support `file://` URL scheme (use an external script or filesystem instead of authentication on CalDAV)
|
||||||
|
|
||||||
## Contributing
|
## 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).
|
If you find bugs, report them on the [issue tracker on GitHub](https://github.com/Maarrk/silverbullet-icalendar/issues).
|
||||||
|
|
||||||
### Building from source
|
### 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
|
```shell
|
||||||
deno task build
|
deno task build
|
||||||
```
|
```
|
||||||
|
|
||||||
Or to watch for changes and rebuild automatically
|
Or to watch for changes and rebuild automatically:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
deno task watch
|
deno task watch
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, copy the resulting `.plug.js` file into your space's `_plug` folder. Or build and copy in one 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.
|
||||||
|
|
||||||
```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).
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
25
deno.json
Normal file
25
deno.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
deno.jsonc
22
deno.jsonc
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
icalendar.plug.js
Normal file
4
icalendar.plug.js
Normal file
File diff suppressed because one or more lines are too long
@@ -2,15 +2,28 @@ name: icalendar
|
|||||||
requiredPermissions:
|
requiredPermissions:
|
||||||
- fetch
|
- fetch
|
||||||
functions:
|
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:
|
showVersion:
|
||||||
path: ./icalendar.ts:showVersion
|
path: ./icalendar.ts:showVersion
|
||||||
command:
|
command:
|
||||||
name: "iCalendar: Version"
|
name: "iCalendar: Version"
|
||||||
priority: -2
|
priority: -2
|
||||||
queryEvents:
|
|
||||||
path: ./icalendar.ts:queryEvents
|
|
||||||
events:
|
|
||||||
- query:ical-event
|
|
||||||
config:
|
config:
|
||||||
schema.config.properties.icalendar:
|
schema.config.properties.icalendar:
|
||||||
type: object
|
type: object
|
||||||
@@ -29,3 +42,6 @@ config:
|
|||||||
type: string
|
type: string
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
|
cacheDuration:
|
||||||
|
type: number
|
||||||
|
description: "Interval between two calendar synchronizations (default: 21600 = 6 hours)"
|
||||||
|
|||||||
300
icalendar.ts
300
icalendar.ts
@@ -1,127 +1,199 @@
|
|||||||
import { editor, system } from "@silverbulletmd/silverbullet/syscalls";
|
import { clientStore, config, datastore, editor, index } from "@silverbulletmd/silverbullet/syscalls";
|
||||||
import { QueryProviderEvent } from "@silverbulletmd/silverbullet/types";
|
import { convertIcsCalendar, type IcsCalendar, type IcsEvent, type IcsDateObjects } from "ts-ics";
|
||||||
import { applyQuery } from "@silverbulletmd/silverbullet/lib/query";
|
|
||||||
import { parseIcsCalendar, type VCalendar } from "ts-ics";
|
|
||||||
|
|
||||||
const VERSION = "0.1.0";
|
const VERSION = "0.2.14";
|
||||||
|
const CACHE_KEY = "icalendar:lastSync";
|
||||||
|
const DEFAULT_CACHE_DURATION_SECONDS = 21600; // 6 hours
|
||||||
|
|
||||||
// Try to match SilverBullet properties where possible.
|
console.log(`[iCalendar] Plug loading (Version ${VERSION})...`);
|
||||||
// Timestamps should be strings formatted with `localDateString`
|
|
||||||
interface Event {
|
|
||||||
// Typically available in calendar apps
|
|
||||||
summary: string | undefined;
|
|
||||||
description: string | undefined;
|
|
||||||
location: string | undefined;
|
|
||||||
|
|
||||||
// Same as SilverBullet pages
|
// ============================================================================
|
||||||
created: string | undefined;
|
// Types
|
||||||
lastModified: string | undefined;
|
// ============================================================================
|
||||||
// Keep consistent with dates above
|
|
||||||
start: string | undefined;
|
|
||||||
end: string | undefined;
|
|
||||||
|
|
||||||
|
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;
|
sourceName: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Source {
|
// ============================================================================
|
||||||
url: string; // Should be an .ics file
|
// Utility Functions
|
||||||
name: string | undefined; // Optional name that will be assigned to events
|
// ============================================================================
|
||||||
}
|
|
||||||
|
|
||||||
export async function queryEvents(
|
/**
|
||||||
{ query }: QueryProviderEvent,
|
* Standard SilverBullet local date string formatter
|
||||||
): Promise<any[]> {
|
*/
|
||||||
const events: Event[] = [];
|
function toLocalISO(d: Date): string {
|
||||||
|
const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
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 {
|
|
||||||
return d.getFullYear() +
|
return d.getFullYear() +
|
||||||
"-" + String(d.getMonth() + 1).padStart(2, "0") +
|
"-" + pad(d.getMonth() + 1) +
|
||||||
"-" + String(d.getDate()).padStart(2, "0") +
|
"-" + pad(d.getDate()) +
|
||||||
"T" + String(d.getHours()).padStart(2, "0") +
|
"T" + pad(d.getHours()) +
|
||||||
":" + String(d.getMinutes()).padStart(2, "0") +
|
":" + pad(d.getMinutes()) +
|
||||||
":" + String(d.getSeconds()).padStart(2, "0") +
|
":" + pad(d.getSeconds()) +
|
||||||
"." + String(d.getMilliseconds()).padStart(3, "0");
|
"." + String(d.getMilliseconds()).padStart(3, "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showVersion() {
|
/**
|
||||||
await editor.flashNotification(`iCalendar Plug ${VERSION}`);
|
* Simplified date processor that trusts the parsed Date object
|
||||||
|
*/
|
||||||
|
function processIcsDate(obj: any, manualShift = 0): string {
|
||||||
|
if (!obj) return "";
|
||||||
|
|
||||||
|
// The 'date' property from ts-ics is already a native JS Date object
|
||||||
|
// which has been parsed with the correct TZID context if available.
|
||||||
|
const d = obj.date instanceof Date ? obj.date : (obj instanceof Date ? obj : null);
|
||||||
|
|
||||||
|
if (!d) return "";
|
||||||
|
|
||||||
|
// Apply ONLY the user requested shift
|
||||||
|
const shiftedDate = new Date(d.getTime() + (manualShift * 3600000));
|
||||||
|
|
||||||
|
return toLocalISO(shiftedDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
BIN
url-nextcloud.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
Reference in New Issue
Block a user