ability

package
v0.92.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 6, 2026 License: GPL-3.0 Imports: 26 Imported by: 0

Documentation

Overview

Package ability provides the capability invocation framework.

Index

Constants

View Source
const (
	OpExampleList   = "list"
	OpExampleGet    = "get"
	OpExampleCreate = "create"
	OpExampleUpdate = "update"
	OpExampleDelete = "delete"
	OpExampleHealth = "health"
)

Example operations as package-level constants.

View Source
const (
	OpBookmarkList       = "list"
	OpBookmarkGet        = "get"
	OpBookmarkCreate     = "create"
	OpBookmarkDelete     = "delete"
	OpBookmarkArchive    = "archive"
	OpBookmarkSearch     = "search"
	OpBookmarkAttachTags = "attach_tags"
	OpBookmarkDetachTags = "detach_tags"
	OpBookmarkCheckURL   = "check_url"
)

Bookmark operations as package-level constants for direct use.

View Source
const (
	OpArchiveAdd    = "add"
	OpArchiveSearch = "search"
	OpArchiveGet    = "get"
)

Archive operations as package-level constants.

View Source
const (
	OpReaderListFeeds       = "list_feeds"
	OpReaderCreateFeed      = "create_feed"
	OpReaderListEntries     = "list_entries"
	OpReaderMarkEntryRead   = "mark_entry_read"
	OpReaderMarkEntryUnread = "mark_entry_unread"
	OpReaderStarEntry       = "star_entry"
	OpReaderUnstarEntry     = "unstar_entry"
)

Reader operations as package-level constants.

View Source
const (
	OpKanbanListTasks    = "list_tasks"
	OpKanbanGetTask      = "get_task"
	OpKanbanCreateTask   = "create_task"
	OpKanbanUpdateTask   = "update_task"
	OpKanbanDeleteTask   = "delete_task"
	OpKanbanMoveTask     = "move_task"
	OpKanbanCompleteTask = "complete_task"
	OpKanbanGetColumns   = "get_columns"
	OpKanbanSearchTasks  = "search_tasks"
)

Kanban operations as package-level constants.

View Source
const (
	OpGithubGetUser           = "get_user"
	OpGithubGetUserByLogin    = "get_user_by_login"
	OpGithubGetRepo           = "get_repo"
	OpGithubListIssues        = "list_issues"
	OpGithubGetIssue          = "get_issue"
	OpGithubGetCommitDiff     = "get_commit_diff"
	OpGithubGetFileContent    = "get_file_content"
	OpGithubListNotifications = "list_notifications"
	OpGithubListReleases      = "list_releases"
)

Github operations as package-level constants.

View Source
const (
	OpForgeGetUser        = "get_user"
	OpForgeGetRepo        = "get_repo"
	OpForgeListIssues     = "list_issues"
	OpForgeGetIssue       = "get_issue"
	OpForgeGetCommitDiff  = "get_commit_diff"
	OpForgeGetFileContent = "get_file_content"
)

Forge operations as package-level constants.

View Source
const (
	OpNotifySend   = "send"
	OpNotifyDigest = "digest"
)

Notify operations as package-level constants.

View Source
const (
	OpNoteList       = "list"
	OpNoteGet        = "get"
	OpNoteCreate     = "create"
	OpNoteUpdate     = "update"
	OpNoteDelete     = "delete"
	OpNoteGetContent = "get_content"
	OpNoteSetContent = "set_content"
	OpNoteSearch     = "search"
	OpNoteGetAppInfo = "get_app_info"
)

Note operations as package-level constants.

View Source
const (
	OpMemoList   = "list"
	OpMemoGet    = "get"
	OpMemoCreate = "create"
	OpMemoUpdate = "update"
	OpMemoDelete = "delete"
	OpMemoHealth = "health"
)

Memo operations as package-level constants.

View Source
const (
	OpFinanceCreateTransaction = "create_transaction"
)

Finance operations as package-level constants.

Variables

View Source
var DefaultRegistry = NewRegistry()
View Source
var Operations = map[hub.CapabilityType]map[string]string{
	hub.CapExample: {
		"List":   "list",
		"Get":    "get",
		"Create": "create",
		"Update": "update",
		"Delete": "delete",
		"Health": "health",
	},
	hub.CapBookmark: {
		"List":       "list",
		"Get":        "get",
		"Create":     "create",
		"Delete":     "delete",
		"Archive":    "archive",
		"Search":     "search",
		"AttachTags": "attach_tags",
		"DetachTags": "detach_tags",
		"CheckURL":   "check_url",
	},
	hub.CapArchive: {
		"Add":    "add",
		"Search": "search",
		"Get":    "get",
	},
	hub.CapReader: {
		"ListFeeds":       "list_feeds",
		"CreateFeed":      "create_feed",
		"ListEntries":     "list_entries",
		"MarkEntryRead":   "mark_entry_read",
		"MarkEntryUnread": "mark_entry_unread",
		"StarEntry":       "star_entry",
		"UnstarEntry":     "unstar_entry",
	},
	hub.CapKanban: {
		"ListTasks":    "list_tasks",
		"GetTask":      "get_task",
		"CreateTask":   "create_task",
		"UpdateTask":   "update_task",
		"DeleteTask":   "delete_task",
		"MoveTask":     "move_task",
		"CompleteTask": "complete_task",
		"GetColumns":   "get_columns",
		"SearchTasks":  "search_tasks",
	},
	hub.CapFinance: {
		"CreateTransaction": "create_transaction",
	},
	hub.CapForge: {
		"GetUser":        "get_user",
		"GetRepo":        "get_repo",
		"ListIssues":     "list_issues",
		"GetIssue":       "get_issue",
		"GetCommitDiff":  "get_commit_diff",
		"GetFileContent": "get_file_content",
	},
	hub.CapGithub: {
		"GetUser":           "get_user",
		"GetUserByLogin":    "get_user_by_login",
		"GetRepo":           "get_repo",
		"ListIssues":        "list_issues",
		"GetIssue":          "get_issue",
		"GetCommitDiff":     "get_commit_diff",
		"GetFileContent":    "get_file_content",
		"ListNotifications": "list_notifications",
		"ListReleases":      "list_releases",
	},
	hub.CapNotify: {
		"Send":   "send",
		"Digest": "digest",
	},
	hub.CapNote: {
		"List":       "list",
		"Get":        "get",
		"Create":     "create",
		"Update":     "update",
		"Delete":     "delete",
		"GetContent": "get_content",
		"SetContent": "set_content",
		"Search":     "search",
		"GetAppInfo": "get_app_info",
	},
	hub.CapMemo: {
		"List":   "list",
		"Get":    "get",
		"Create": "create",
		"Update": "update",
		"Delete": "delete",
		"Health": "health",
	},
}

Operations returns a capability-specific operation constant. All ability operations are defined here to avoid import namespace conflicts with internal/modules packages.

Functions

func BoolParam

func BoolParam(params map[string]any, key string) (bool, bool)

func EncodeCursor

func EncodeCursor(secret []byte, payload CursorPayload) (string, error)

func GetEventPool

func GetEventPool() *ants.PoolWithFunc

GetEventPool returns the global event pool, or nil if not initialized.

func InitEventPool

func InitEventPool(size int, expiryDuration string, mc *metrics.AbilityCollector) error

InitEventPool creates the global event pool. Must be called once during startup. Call ShutdownEventPool during server shutdown.

func Int64Param

func Int64Param(params map[string]any, key string) (int64, bool)

func IntParam

func IntParam(params map[string]any, key string) (int, bool)

func IsMutation

func IsMutation(op string) bool

IsMutation reports whether the operation name indicates a write that modifies state.

func Op

func Op(capType hub.CapabilityType, key string) string

Op returns the string operation name for the given capability and operation key. Example: ability.Op(hub.CapBookmark, "List") returns "list".

func RegisterInvoker

func RegisterInvoker(capability hub.CapabilityType, operation string, invoker Invoker) error

func RequiredInt

func RequiredInt(params map[string]any, key string) (int, error)

func RequiredInt64

func RequiredInt64(params map[string]any, key string) (int64, error)

func RequiredString

func RequiredString(params map[string]any, key string) (string, error)

func SetBulkheadCallbacks

func SetBulkheadCallbacks()

SetBulkheadCallbacks wires the bulkhead manager with metrics reporting callbacks.

func SetEventEmitter

func SetEventEmitter(emitter EventEmitter)

func SetEventSourceManager

func SetEventSourceManager(m *EventSourceManager)

SetEventSourceManager stores the EventSourceManager for cross-package access. Must be called during server startup before modules Bootstrap.

func SetMetricsCollector

func SetMetricsCollector(mc *metrics.AbilityCollector)

SetMetricsCollector sets the AbilityCollector on the DefaultRegistry.

func ShutdownEventPool

func ShutdownEventPool()

ShutdownEventPool releases the pool, waiting up to 30s for in-flight tasks.

func StringParam

func StringParam(params map[string]any, key string) (string, bool)

func UnregisterInvoker

func UnregisterInvoker(capability hub.CapabilityType, operation string)

UnregisterInvoker removes an invoker for a capability and operation.

Types

type ArchiveItem

type ArchiveItem struct {
	ID        string    `json:"id"`
	URL       string    `json:"url"`
	Title     string    `json:"title,omitzero"`
	Status    string    `json:"status"`
	CreatedAt time.Time `json:"created_at"`
}

type Bookmark

type Bookmark struct {
	ID         string    `json:"id"`
	URL        string    `json:"url"`
	Title      string    `json:"title,omitzero"`
	Summary    string    `json:"summary,omitzero"`
	Tags       []string  `json:"tags,omitzero"`
	Archived   bool      `json:"archived"`
	Favourited bool      `json:"favourited"`
	CreatedAt  time.Time `json:"created_at"`
}

type CursorPayload

type CursorPayload struct {
	Capability     string    `json:"capability,omitempty"`
	Backend        string    `json:"backend,omitempty"`
	Strategy       string    `json:"strategy,omitempty"`
	ProviderCursor string    `json:"provider_cursor,omitempty"`
	Page           int       `json:"page,omitempty"`
	Offset         int       `json:"offset,omitempty"`
	Limit          int       `json:"limit,omitempty"`
	SortBy         string    `json:"sort_by,omitempty"`
	SortOrder      string    `json:"sort_order,omitempty"`
	FilterHash     string    `json:"filter_hash,omitempty"`
	ExpiresAt      time.Time `json:"expires_at"`
}

func DecodeCursor

func DecodeCursor(secret []byte, cursor string, now time.Time) (CursorPayload, error)

type Entry

type Entry struct {
	ID          int64     `json:"id"`
	Title       string    `json:"title"`
	URL         string    `json:"url"`
	Content     string    `json:"content,omitzero"`
	Status      string    `json:"status"`
	Starred     bool      `json:"starred"`
	PublishedAt time.Time `json:"published_at"`
	FeedTitle   string    `json:"feed_title,omitzero"`
}

type EventEmitter

type EventEmitter func(ctx context.Context, result *InvokeResult)

type EventRef

type EventRef struct {
	EventID   string `json:"event_id"`
	EventType string `json:"event_type"`
	EntityID  string `json:"entity_id,omitzero"`
}

type EventSourceEmitter

type EventSourceEmitter func(ctx context.Context, events []types.DataEvent) error

EventSourceEmitter is the function signature for emitting DataEvents produced by webhook converters and polling resources.

type EventSourceManager

type EventSourceManager struct {
	// contains filtered or unexported fields
}

EventSourceManager orchestrates webhook converters and polling resources, dispatching their output through the EventEmitter chain.

func GetEventSourceManager

func GetEventSourceManager() *EventSourceManager

GetEventSourceManager returns the global EventSourceManager, or nil if not set.

func NewEventSourceManager

func NewEventSourceManager(
	emitter EventSourceEmitter,
	stateStore *PollingState,
	mc *metrics.EventSourceCollector,
) *EventSourceManager

NewEventSourceManager creates an EventSourceManager backed by the given emitter, state store, and metrics collector.

func (*EventSourceManager) RegisterPolling

func (m *EventSourceManager) RegisterPolling(r PollingResource)

RegisterPolling registers a polling resource. The poll interval is derived from the resource's DefaultInterval method. Nil resources are silently skipped.

func (*EventSourceManager) RegisterWebhook

func (m *EventSourceManager) RegisterWebhook(c WebhookConverter)

RegisterWebhook registers a webhook converter.

func (*EventSourceManager) SetPool

func (m *EventSourceManager) SetPool(pool *ants.PoolWithFunc)

SetPool assigns the event pool for non-blocking webhook event submission.

func (*EventSourceManager) Start

func (m *EventSourceManager) Start(ctx context.Context) error

Start begins the cron scheduler, loads persisted state, and starts periodic flush.

func (*EventSourceManager) Stop

func (m *EventSourceManager) Stop(ctx context.Context) error

Stop shuts down the cron scheduler, flushes state, and releases the event pool.

func (*EventSourceManager) WebhookHandler

func (m *EventSourceManager) WebhookHandler() fiber.Handler

WebhookHandler returns a Fiber handler that dispatches incoming webhook requests to the registered WebhookConverter for the given path.

type EventStore

type EventStore interface {
	AppendDataEvent(ctx context.Context, event types.DataEvent) error
	AppendEventOutbox(ctx context.Context, event types.DataEvent) error
}

EventStore abstracts the persistence of DataEvent records.

type Feed

type Feed struct {
	ID       int64  `json:"id"`
	Title    string `json:"title"`
	FeedURL  string `json:"feed_url"`
	SiteURL  string `json:"site_url,omitzero"`
	Category string `json:"category,omitzero"`
}

type ForgeCommitDiff

type ForgeCommitDiff struct {
	CommitID      string   `json:"commit_id"`
	CommitMessage string   `json:"commit_message"`
	Files         []string `json:"files"`
	DiffContent   string   `json:"diff_content"`
}

ForgeCommitDiff represents a commit diff on a software forge.

type ForgeIssue

type ForgeIssue struct {
	ID      int64  `json:"id"`
	Index   int64  `json:"number"`
	Title   string `json:"title"`
	Body    string `json:"body,omitzero"`
	State   string `json:"state"`
	HTMLURL string `json:"html_url"`
	Author  string `json:"author,omitzero"`
}

ForgeIssue represents an issue on a software forge.

type ForgeRepo

type ForgeRepo struct {
	ID          int64  `json:"id"`
	Name        string `json:"name"`
	FullName    string `json:"full_name"`
	Description string `json:"description,omitzero"`
	Private     bool   `json:"private"`
	HTMLURL     string `json:"html_url"`
	CloneURL    string `json:"clone_url,omitzero"`
	Owner       string `json:"owner"`
}

ForgeRepo represents a repository on a software forge.

type ForgeUser

type ForgeUser struct {
	ID        int64  `json:"id"`
	UserName  string `json:"username"`
	Email     string `json:"email,omitzero"`
	AvatarURL string `json:"avatar_url,omitzero"`
}

ForgeUser represents a forge user account.

type Host

type Host struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	Address string `json:"address,omitzero"`
	Status  string `json:"status"`
}

type InvokeResult

type InvokeResult struct {
	Capability hub.CapabilityType `json:"capability"`
	Operation  string             `json:"operation"`
	Data       any                `json:"data,omitzero"`
	Page       *PageInfo          `json:"page,omitzero"`
	Text       string             `json:"text,omitzero"`
	Meta       map[string]any     `json:"meta,omitzero"`
	Events     []EventRef         `json:"events,omitzero"`
	Resource   *ResourceMeta      `json:"_resource,omitempty"`
}

func Invoke

func Invoke(ctx context.Context, capability hub.CapabilityType, operation string, params map[string]any) (*InvokeResult, error)

type Invoker

type Invoker func(ctx context.Context, params map[string]any) (*InvokeResult, error)

type ListResult

type ListResult[T any] struct {
	Items []*T      `json:"items"`
	Page  *PageInfo `json:"page,omitzero"`
}

type Memo

type Memo struct {
	Name       string    `json:"name"`
	State      string    `json:"state,omitzero"`
	Content    string    `json:"content,omitzero"`
	Visibility string    `json:"visibility,omitzero"`
	Tags       []string  `json:"tags,omitzero"`
	Pinned     bool      `json:"pinned"`
	Creator    string    `json:"creator,omitzero"`
	Snippet    string    `json:"snippet,omitzero"`
	CreateTime time.Time `json:"create_time,omitzero"`
	UpdateTime time.Time `json:"update_time,omitzero"`
}

Memo represents a memo from a note-taking system such as Memos.

type Note

type Note struct {
	ID              string   `json:"id"`
	Title           string   `json:"title"`
	Type            string   `json:"type,omitzero"`
	Content         string   `json:"content,omitzero"`
	ParentNoteIDs   []string `json:"parent_note_ids,omitzero"`
	ChildNoteIDs    []string `json:"child_note_ids,omitzero"`
	IsProtected     bool     `json:"is_protected"`
	DateCreated     string   `json:"date_created,omitzero"`
	DateModified    string   `json:"date_modified,omitzero"`
	UtcDateCreated  string   `json:"utc_date_created,omitzero"`
	UtcDateModified string   `json:"utc_date_modified,omitzero"`
}

Note represents a note from a note-taking system such as Trilium.

type Notification

type Notification struct {
	ID         string    `json:"id"`
	Reason     string    `json:"reason,omitzero"`
	Unread     bool      `json:"unread"`
	Subject    string    `json:"subject,omitzero"`
	RepoName   string    `json:"repo_name,omitzero"`
	UpdatedAt  time.Time `json:"updated_at"`
	LastReadAt time.Time `json:"last_read_at,omitzero"`
}

Notification represents a GitHub notification.

type PageInfo

type PageInfo struct {
	Limit      int    `json:"limit"`
	HasMore    bool   `json:"has_more"`
	NextCursor string `json:"next_cursor,omitzero"`
	PrevCursor string `json:"prev_cursor,omitzero"`
	Total      *int64 `json:"total,omitzero"`
}

type PageRequest

type PageRequest struct {
	Limit     int    `json:"limit,omitzero"`
	Cursor    string `json:"cursor,omitzero"`
	SortBy    string `json:"sort_by,omitzero"`
	SortOrder string `json:"sort_order,omitzero"`
}

func PageRequestFromParams

func PageRequestFromParams(params map[string]any) PageRequest

type Persistence

type Persistence interface {
	LoadAll(ctx context.Context) (map[string]PollingEntry, error)
	Save(ctx context.Context, resourceName, cursor string, knownHashes map[string]string) error
}

Persistence defines the backend storage interface for polling state.

type PollResult

type PollResult struct {
	Items      []any
	NextCursor string
	HasMore    bool
}

PollResult carries a batch of items returned by a polling List call.

type PollingEntry

type PollingEntry struct {
	Cursor      string
	KnownHashes map[string]string
	UpdatedAt   time.Time
}

PollingEntry holds cursor position and known content hashes for one polling resource.

type PollingResource

type PollingResource interface {
	// ResourceName returns a unique name for the polled resource type.
	ResourceName() string
	// DefaultInterval returns the recommended polling interval for this resource.
	DefaultInterval() time.Duration
	// DiffKey returns a unique key from an item used to detect changes between polls.
	DiffKey(item any) string
	// ContentHash returns a hash of the item content for change detection.
	ContentHash(item any) string
	// CursorField returns the field name used for cursor-based pagination.
	CursorField() string
	// List fetches a batch of items from the provider starting after cursor.
	List(ctx context.Context, cursor string) (PollResult, error)
}

PollingResource represents a single pollable resource type from a provider. Each (provider, resource) pair registers one PollingResource.

type PollingState

type PollingState struct {
	// contains filtered or unexported fields
}

PollingState manages in-memory polling state with periodic persistence. Each pollEntry has its own lock to avoid global contention.

func NewPollingState

func NewPollingState(backend Persistence) *PollingState

NewPollingState creates a PollingState backed by the given Persistence.

func (*PollingState) Flush

func (s *PollingState) Flush(ctx context.Context) error

Flush persists all dirty entries to the backend. It attempts to save every dirty entry and collects errors so that a single failure does not abandon remaining entries.

func (*PollingState) FlushInterval

func (*PollingState) FlushInterval() time.Duration

FlushInterval returns the recommended interval between periodic flushes.

func (*PollingState) Get

func (s *PollingState) Get(name string) PollingEntry

Get returns a copy of the polling entry for the named resource. Returns an empty entry if the resource is unknown.

func (*PollingState) Load

func (s *PollingState) Load(ctx context.Context) error

Load restores state from the persistence backend. It overwrites any in-memory entries with the persisted data — this is intended to be called once during startup before any polls run, so persisted state takes precedence over in-memory defaults.

func (*PollingState) MarkDirty

func (s *PollingState) MarkDirty(name string)

MarkDirty marks a resource as needing persistence.

func (*PollingState) Update

func (s *PollingState) Update(name string, entry PollingEntry)

Update sets the polling entry for the named resource.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

func NewRegistry

func NewRegistry() *Registry

func (*Registry) Invoke

func (r *Registry) Invoke(ctx context.Context, capability hub.CapabilityType, operation string, params map[string]any) (*InvokeResult, error)

func (*Registry) Register

func (r *Registry) Register(capability hub.CapabilityType, operation string, invoker Invoker) error

func (*Registry) Unregister

func (r *Registry) Unregister(capability hub.CapabilityType, operation string)

Unregister removes an invoker for a capability and operation.

type Release

type Release struct {
	ID          int64     `json:"id"`
	TagName     string    `json:"tag_name"`
	Name        string    `json:"name,omitzero"`
	Body        string    `json:"body,omitzero"`
	Draft       bool      `json:"draft"`
	Prerelease  bool      `json:"prerelease"`
	HTMLURL     string    `json:"html_url,omitzero"`
	PublishedAt time.Time `json:"published_at,omitzero"`
}

Release represents a GitHub repository release.

type ResourceMeta

type ResourceMeta struct {
	EventID  string `json:"event_id"`
	EntityID string `json:"entity_id"`
	App      string `json:"app"`
}

ResourceMeta identifies a resource created by a capability mutation operation.

type Task

type Task struct {
	ID          int      `json:"id"`
	Title       string   `json:"title"`
	Description string   `json:"description,omitzero"`
	ProjectID   int      `json:"project_id"`
	ColumnID    int      `json:"column_id"`
	Tags        []string `json:"tags,omitzero"`
	Reference   string   `json:"reference,omitzero"`
}

type WebhookConverter

type WebhookConverter interface {
	// WebhookPath returns the URL path that the webhook endpoint listens on.
	WebhookPath() string
	// VerifySignature validates the incoming webhook payload against the provider's signing scheme.
	VerifySignature(headers map[string]string, body []byte) error
	// Convert transforms a raw webhook payload into one or more DataEvent records.
	Convert(body []byte, headers map[string]string) ([]types.DataEvent, error)
}

WebhookConverter converts a provider-specific webhook payload into DataEvent records. Each implementation encapsulates its own signature verification scheme.

Directories

Path Synopsis
Package bookmark implements the bookmark management capability.
Package bookmark implements the bookmark management capability.
karakeep
Package karakeep implements the Karakeep adapter for the bookmark capability.
Package karakeep implements the Karakeep adapter for the bookmark capability.
Package conformance provides a standard test suite for ability adapters.
Package conformance provides a standard test suite for ability adapters.
Package example implements the example provider adapter for the example capability.
Package example implements the example provider adapter for the example capability.
Package forge implements the software forge capability.
Package forge implements the software forge capability.
gitea
Package gitea implements the Gitea adapter for the forge capability.
Package gitea implements the Gitea adapter for the forge capability.
Package github implements the GitHub adapter for the github capability.
Package github implements the GitHub adapter for the github capability.
Package kanban implements the Kanban board capability.
Package kanban implements the Kanban board capability.
kanboard
Package kanboard implements the Kanboard adapter for the kanban capability.
Package kanboard implements the Kanboard adapter for the kanban capability.
Package memo implements the memo capability for short-form note-taking systems.
Package memo implements the memo capability for short-form note-taking systems.
memos
Package memos implements the Memos adapter for the memo capability.
Package memos implements the Memos adapter for the memo capability.
Package note implements the note capability for note-taking systems.
Package note implements the note capability for note-taking systems.
trilium
Package trilium implements the Trilium adapter for the note capability.
Package trilium implements the Trilium adapter for the note capability.
Package notify provides the notification capability for the ability framework.
Package notify provides the notification capability for the ability framework.
Package reader implements the RSS/feed reading capability.
Package reader implements the RSS/feed reading capability.
miniflux
Package miniflux implements the Miniflux adapter for the reader capability.
Package miniflux implements the Miniflux adapter for the reader capability.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL