-
Notifications
You must be signed in to change notification settings - Fork 12k
feat: add nostr calendar integration #24266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
patrickulrich
wants to merge
1
commit into
calcom:main
Choose a base branch
from
patrickulrich:nostrcal
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,136
−1
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| import Image from "next/image"; | ||
| import { useRouter } from "next/navigation"; | ||
| import { useState } from "react"; | ||
| import { useForm } from "react-hook-form"; | ||
| import { Toaster } from "sonner"; | ||
|
|
||
| import { useLocale } from "@calcom/lib/hooks/useLocale"; | ||
| import { Alert } from "@calcom/ui/components/alert"; | ||
| import { Button } from "@calcom/ui/components/button"; | ||
| import { Form, TextField } from "@calcom/ui/components/form"; | ||
| import { Icon } from "@calcom/ui/components/icon"; | ||
|
|
||
| interface FormData { | ||
| authMethod: "bunker" | "nsec"; | ||
| bunkerUri?: string; | ||
| nsec?: string; | ||
| } | ||
|
|
||
| export default function NostrSetup() { | ||
| const { t } = useLocale(); | ||
| const router = useRouter(); | ||
| const form = useForm<FormData>({ | ||
| defaultValues: { | ||
| authMethod: "bunker", // Default to bunker as recommended | ||
| bunkerUri: "", | ||
| nsec: "", | ||
| }, | ||
| }); | ||
|
|
||
| const [errorMessage, setErrorMessage] = useState(""); | ||
| const authMethod = form.watch("authMethod"); | ||
|
|
||
| return ( | ||
| <div className="bg-emphasis flex h-screen"> | ||
| <div className="bg-default m-auto rounded p-5 md:w-[560px] md:p-10"> | ||
| <div className="flex flex-col space-y-5 md:flex-row md:space-x-5 md:space-y-0"> | ||
| <div> | ||
| <Image | ||
| src="/api/app-store/nostrcalendar/icon.svg" | ||
| alt="Nostr" | ||
| width={48} | ||
| height={48} | ||
| className="h-12 w-12 max-w-2xl" | ||
| /> | ||
| </div> | ||
| <div className="flex w-10/12 flex-col"> | ||
| <h1 className="text-default">Connect to Nostr</h1> | ||
| <div className="mt-1 text-sm"> | ||
| Choose how you want to authenticate with Nostr for managing your calendar events. | ||
| </div> | ||
|
|
||
| <div className="my-2 mt-3"> | ||
| <Form | ||
| form={form} | ||
| handleSubmit={async (values) => { | ||
| setErrorMessage(""); | ||
|
|
||
| const res = await fetch("/api/integrations/nostrcalendar/add", { | ||
| method: "POST", | ||
| body: JSON.stringify({ | ||
| authMethod: values.authMethod, | ||
| bunkerUri: values.bunkerUri, | ||
| nsec: values.nsec, | ||
| }), | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| }); | ||
|
|
||
| const json = await res.json(); | ||
| if (!res.ok) { | ||
| setErrorMessage(json?.message || t("something_went_wrong")); | ||
| } else { | ||
| router.push(json.url); | ||
| } | ||
| }}> | ||
| <fieldset className="space-y-4" disabled={form.formState.isSubmitting}> | ||
| {/* Authentication Method Selection */} | ||
| <div className="space-y-3"> | ||
| <label className="text-default text-sm font-medium">Authentication Method</label> | ||
|
|
||
| <label className="border-default hover:bg-emphasis flex cursor-pointer items-start space-x-3 rounded-md border p-3 transition-colors"> | ||
| <input type="radio" value="bunker" {...form.register("authMethod")} className="mt-1" /> | ||
| <div className="flex-1"> | ||
| <div className="text-default font-medium"> | ||
| Bunker Connection{" "} | ||
| <span className="bg-success text-success ml-2 rounded px-2 py-0.5 text-xs font-medium"> | ||
| Recommended | ||
| </span> | ||
| </div> | ||
| <div className="text-subtle mt-1 text-sm"> | ||
| Keep your keys secure in a remote signer. Supports all features including private | ||
| events. | ||
| </div> | ||
| </div> | ||
| </label> | ||
|
|
||
| <label className="border-default hover:bg-emphasis flex cursor-pointer items-start space-x-3 rounded-md border p-3 transition-colors"> | ||
| <input type="radio" value="nsec" {...form.register("authMethod")} className="mt-1" /> | ||
| <div className="flex-1"> | ||
| <div className="text-default font-medium">Private Key (nsec)</div> | ||
| <div className="text-subtle mt-1 text-sm"> | ||
| Store encrypted key locally. Supports all features including private events. | ||
| </div> | ||
| </div> | ||
| </label> | ||
| </div> | ||
|
|
||
| {/* Conditional Input Fields */} | ||
| {authMethod === "bunker" ? ( | ||
| <> | ||
| <TextField | ||
| required | ||
| {...form.register("bunkerUri")} | ||
| label="Bunker URI" | ||
| placeholder="bunker://... or user@domain.com" | ||
| autoComplete="off" | ||
| /> | ||
|
|
||
| <div className="rounded bg-blue-50 p-3 text-sm text-blue-700 dark:bg-blue-950 dark:text-blue-300"> | ||
| <Icon name="info" className="mb-0.5 inline-flex h-4 w-4" /> Your keys stay secure in | ||
| the bunker. You'll need to approve the connection in your bunker app. | ||
| </div> | ||
|
|
||
| <details className="text-subtle text-sm"> | ||
| <summary className="cursor-pointer font-medium"> | ||
| What permissions will be requested? | ||
| </summary> | ||
| <ul className="mt-2 space-y-1 pl-5"> | ||
| <li>• Sign calendar events (kinds 31922, 31923, 31927)</li> | ||
| <li>• Sign seals for private events (kind 13)</li> | ||
| <li>• Sign deletion events (kind 5)</li> | ||
| <li>• Encrypt/decrypt content (NIP-44)</li> | ||
| </ul> | ||
| </details> | ||
| </> | ||
| ) : ( | ||
| <> | ||
| <TextField | ||
| required | ||
| type="password" | ||
| {...form.register("nsec")} | ||
| label="Nostr Private Key (nsec)" | ||
| placeholder="nsec1..." | ||
| autoComplete="off" | ||
| /> | ||
|
|
||
| <div className="rounded bg-blue-50 p-3 text-sm text-blue-700 dark:bg-blue-950 dark:text-blue-300"> | ||
| <Icon name="info" className="mb-0.5 inline-flex h-4 w-4" /> Your nsec key will be | ||
| encrypted before storage. Never share this key with anyone. | ||
| </div> | ||
| </> | ||
| )} | ||
|
|
||
| <div className="rounded bg-blue-50 p-3 text-sm text-blue-700 dark:bg-blue-950 dark:text-blue-300"> | ||
| <Icon name="info" className="mb-0.5 inline-flex h-4 w-4" /> Your relay list will be | ||
| automatically discovered from your kind 10002 relay list metadata. | ||
| </div> | ||
| </fieldset> | ||
|
|
||
| {errorMessage && <Alert severity="error" title={errorMessage} className="my-4" />} | ||
|
|
||
| <div className="mt-5 justify-end space-x-2 rtl:space-x-reverse sm:mt-4 sm:flex"> | ||
| <Button type="button" color="secondary" onClick={() => router.back()}> | ||
| {t("cancel")} | ||
| </Button> | ||
| <Button type="submit" loading={form.formState.isSubmitting}> | ||
| {t("save")} | ||
| </Button> | ||
| </div> | ||
| </Form> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <Toaster position="bottom-right" /> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| --- | ||
| items: | ||
| - 1.jpeg | ||
| - 2.jpeg | ||
| - 3.jpeg | ||
| --- | ||
|
|
||
| # Nostr Calendar | ||
|
|
||
| Sync your Cal.com bookings with Nostr using the NIP-52 calendar events specification. | ||
|
|
||
| ## What it does | ||
|
|
||
| When someone books a meeting with you on Cal.com, this app: | ||
| - Publishes a private NIP-59 gift-wrapped calendar event (kind 31923) to your Nostr relays | ||
| - Creates a public availability block (kind 31927) to mark you as busy | ||
| - Checks your existing Nostr calendar events to prevent double-bookings | ||
|
|
||
| ## Authentication | ||
|
|
||
| Choose how you want to connect: | ||
|
|
||
| **Bunker (recommended)** - Connect a remote signer like nsec.app or Amber. Your keys stay in the signer and never touch Cal.com's servers. | ||
|
|
||
| **Private Key (nsec)** - Provide your nsec key directly. It's encrypted before storage using the same encryption Cal.com uses for other integrations. | ||
|
|
||
| ## Setup | ||
|
|
||
| 1. Install the app from the Cal.com app store | ||
| 2. Choose bunker or nsec authentication | ||
| 3. Enable "Check for conflicts" in your calendar settings to sync availability | ||
| 4. Done - your relay list is automatically discovered from your kind 10002 metadata | ||
|
|
||
| ## Privacy | ||
|
|
||
| By default, calendar events are created as private NIP-59 gift-wrapped events. Event details are encrypted and only visible to participants. A public availability block (kind 31927) is created so others can see you're busy without seeing the event details. | ||
|
|
||
| ## Implementation | ||
|
|
||
| Implements the following Nostr specs: | ||
| - NIP-52 (Calendar Events) | ||
| - NIP-46 (Nostr Connect / bunker) | ||
| - NIP-59 (Gift Wrap for private events) | ||
| - NIP-44 (Encrypted payloads) | ||
| - NIP-09 (Event deletion) | ||
|
|
||
| Supports all NIP-52 event types: date-based (31922), time-based (31923), RSVPs (31925), and availability blocks (31927). | ||
|
|
||
| ## Learn More | ||
|
|
||
| - [NIP-52 Specification](https://github.com/nostr-protocol/nips/blob/master/52.md) | ||
| - [Nostr Protocol](https://nostr.com) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import type { AppMeta } from "@calcom/types/App"; | ||
|
|
||
| import _package from "./package.json"; | ||
|
|
||
| export const metadata = { | ||
| name: "Nostr", | ||
| description: _package.description, | ||
| installed: true, | ||
| type: "nostr_calendar", | ||
| title: "Nostr Calendar", | ||
| variant: "calendar", | ||
| category: "calendar", | ||
| categories: ["calendar"], | ||
| logo: "icon.svg", | ||
| publisher: "NostrCal.com", | ||
| slug: "nostr", | ||
| url: "https://nostr.com", | ||
| email: "hello@nostrcal.com", | ||
| dirName: "nostrcalendar", | ||
| isOAuth: false, | ||
| } as AppMeta; | ||
|
|
||
| export default metadata; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CRITICAL: Fix slug inconsistency.
There is a critical inconsistency in the slug definition:
"slug": "nostrcalendar"slug: "nostr"This mismatch will cause the app store to fail resolving the integration properly. The slug must be identical across all configuration files.
Additionally, there's a URL inconsistency:
"url": "https://nostrcal.com"url: "https://nostr.com"Apply this diff to align the slug with config.json and fix the URL:
export const metadata = { name: "Nostr", description: _package.description, installed: true, type: "nostr_calendar", title: "Nostr Calendar", variant: "calendar", category: "calendar", categories: ["calendar"], logo: "icon.svg", publisher: "NostrCal.com", - slug: "nostr", - url: "https://nostr.com", + slug: "nostrcalendar", + url: "https://nostrcal.com", email: "hello@nostrcal.com", dirName: "nostrcalendar", isOAuth: false, } as AppMeta;🤖 Prompt for AI Agents