Skip to content

Calendar time zones, recurrence checks and administration; Talk join link, password and lobby - #88

Open
BjoernSchotte wants to merge 6 commits into
cloud-py-api:mainfrom
BjoernSchotte:upstream-pr/calendar-talk
Open

BjoernSchotte wants to merge 6 commits into
cloud-py-api:mainfrom
BjoernSchotte:upstream-pr/calendar-talk

Conversation

@BjoernSchotte

Copy link
Copy Markdown

Motivation

We use this server so that an assistant can plan events for a group: create a calendar, share it, put
(recurring) events in it with a video link, and open a Talk room that guests can join. Doing this for real people
showed some gaps: times without an offset were taken as UTC, a bad RRULE reached Nextcloud, there was no way to
give a Talk room a guest password or lobby, no join link was returned, and nothing kept the assistant from
changing events that other people own. This PR closes these gaps. It is independent of #87 (multi-user mode) and does not need it.

Changes

Talk

  • create_conversation and get_conversation return url, the link to join (<base>/index.php/call/<token>;
    works without pretty URLs; the token is URL-encoded).
  • New set_conversation_password (write): sets the password guests need for a public conversation. Removing the
    password needs destructive. The password is never echoed, also not in error messages.
  • New set_conversation_lobby (write): turns the lobby on (optionally until a future time) or off.

Clearer error for apps that are not available (OCS 998)

Nextcloud answers OCS 998 ("Invalid query, please check the syntax") when no route matches. Under apps/<id>/
this means the app does not serve this account: not installed, disabled, or limited to groups the user is not in
(seen with Talk restricted to some groups). The error now says so. App-level 404s keep their own message.

Calendar events

  • Time zones: NEXTCLOUD_MCP_TIMEZONE (IANA name, validated at start) and a timezone tool argument. Times
    without an offset are wall time in that zone; events get TZID and a VTIMEZONE, so a weekly event stays at
    19:00 across daylight saving changes. Without either setting, times stay UTC as before. get_events accepts
    offsets in the range. Moving only the start keeps the duration; an end before the start is refused.
  • RRULE checks: only DAILY..YEARLY, known parts, COUNT/INTERVAL at most 1000, never COUNT and
    UNTIL together. UNTIL is converted to UTC (or a date for all-day events), as RFC 5545 requires.
  • conference_url: stored as CONFERENCE (RFC 7986) and as LOCATION when that is empty (as Nextcloud
    Calendar does for Talk rooms), else as a Video call: <url> line in the description. Only http(s) URLs are
    accepted. update_event can replace or remove it.
  • Safety for events of other people: create_event marks events with X-NC-MCP-CREATED-BY:<user id>.
    update_event refuses events without the caller's mark, or with attendees (Nextcloud would send them
    notifications), unless allow_foreign=true, so that a client can ask its user first. update_event and
    delete_event take an optional expected_summary and refuse to act if the title differs. Event output shows
    has_attendees and created_by_me, never attendee or organizer addresses.

Calendar administration (new tools)

create_calendar (unique URI from the name, also avoiding URIs of trashed calendars; optional group share in the
same call), share_calendar / unshare_calendar (users and groups, read or read-write), publish_calendar /
unpublish_calendar (public read-only link), delete_calendar (destructive, with an expected_name guard; for a
sharee it removes the calendar from their account only) and get_calendar_shares (owner only).
get_calendar_shares uses Depth 1 on the calendar home, because Nextcloud omits owner-principal with Depth 0.

search_files: refuse ..

The SEARCH scope is now built from the path's segments: empty and . segments are dropped, and .. is refused
with a clear error before a request is sent. Before, a/../.. went to Nextcloud and failed there with an unclear
error. The scope is deliberately not percent-encoded: SearchDAV's PathHelper takes a relative scope href as
a path without URL-decoding it; the existing integration test for a folder named search #1 %41 Ü depends on this.

Security considerations

  • The conversation password is never returned, also not in error messages.
  • Removing a conversation password and deleting a calendar need the destructive level; all other new tools are
    write, get_calendar_shares is read.
  • conference_url accepts only http/https (no javascript: etc.).
  • The creator mark and expected_summary/expected_name guards make it harder for an assistant to change or
    delete the wrong thing in shared calendars. They are guards, not access control: Nextcloud's own permissions
    still decide.
  • .. in search_files paths is refused before a request is sent.

Tests

  • Unit: tests/test_talk_admin.py (join link, password, lobby), tests/test_client_errors.py (OCS 998),
    tests/test_calendar_helpers.py (time zones, RRULE, conference URL, guards, slugs, principals),
    tests/test_files_helpers.py (_search_scope).
  • Integration: tests/integration/test_calendar_guard.py (time zones across DST, bad RRULE creates nothing,
    video link, shared calendar <uri>_shared_by_<owner>: the read-write sharee writes, the owner needs
    allow_foreign, a read-only sharee cannot write) and tests/integration/test_calendar_admin.py (group share
    visible to the member, sharees see no shares and cannot share, publish/unpublish, delete guard), plus the ..
    case in tests/integration/test_files.py. These ran against Nextcloud 34 during development.
  • Locally on Python 3.12: ruff check, ruff format --check, isort --check src/ and pyright are clean; 773
    unit tests pass.
  • README and PROGRESS.md list the 9 new tools; scripts/sync_progress.py maps the new calendar test modules,
    and the counts were recomputed with it.

Backwards compatibility

  • Without NEXTCLOUD_MCP_TIMEZONE and without the timezone argument, times without an offset are UTC, as before.
  • New tool arguments are optional.
  • Behaviour change: update_event now refuses events that this server did not create, or that have attendees,
    unless allow_foreign=true. Events created before this change carry no mark, so updating them needs
    allow_foreign=true once. If you prefer this guard to be opt-in, I am happy to change it.
  • RRULE values that were accepted before but are invalid or unbounded (e.g. COUNT with UNTIL) are now refused.

AI disclosure: this change was written with Claude Code (Claude Opus 5.5); commits carry a Co-Authored-By trailer.

🤖 Generated with Claude Code

BjoernSchotte and others added 6 commits October 7, 2026 14:50
create_conversation and get_conversation return url, the link to join
(<base>/index.php/call/<token>, works without pretty URLs).
set_conversation_password sets the password guests need for a public
conversation; removing it needs the destructive level; the password is
never echoed, also not in errors. set_conversation_lobby turns the lobby
on (optionally until a future time) or off.

Signed-off-by: Björn Schotte <bjoern.schotte@googlemail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nextcloud answers 998 ("Invalid query, please check the syntax") when no
route matches. For apps/<id>/ that means the app does not serve this
account: not installed, disabled, or limited to groups the user is not in
(seen with Talk restricted to some groups). The old message misled the
caller; app-level 404s keep their own message.

Signed-off-by: Björn Schotte <bjoern.schotte@googlemail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- NEXTCLOUD_MCP_TIMEZONE and a timezone argument: times without offset are wall
  time in that zone; events get TZID + VTIMEZONE (recurring events keep their
  wall time across DST). get_events accepts offsets in the range.
- RRULE is checked (DAILY..YEARLY, known parts, COUNT/INTERVAL <= 1000, not
  COUNT and UNTIL together); UNTIL converted to UTC or a date.
- conference_url: CONFERENCE (RFC 7986) plus LOCATION when empty, else a line
  in the description; update can replace or remove it.
- create_event marks X-NC-MCP-CREATED-BY; update_event refuses events of others
  or with attendees unless allow_foreign=true; expected_summary on update and
  delete. Output has has_attendees/created_by_me, never attendee addresses.
- Moving only the start keeps the duration; end before start is refused.

Tests: 43 unit, 6 integration against Nextcloud 34 (shared calendar
<uri>_shared_by_<owner>, read-only sharee cannot write).

Signed-off-by: Björn Schotte <bjoern.schotte@googlemail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ublic link, delete, shares

create_calendar (unique URI, also past trashed calendars; optional group share),
share_calendar/unshare_calendar (users and groups), publish_calendar/unpublish_calendar,
delete_calendar (expected_name guard), get_calendar_shares (owner only; Depth 1 on the
home because Nextcloud omits owner-principal with Depth 0). Tests: unit + integration
against Nextcloud 34 (group share visible to the member, sharees see no shares and
cannot share).

Signed-off-by: Björn Schotte <bjoern.schotte@googlemail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
search_files built its SEARCH scope from the path as given, so "a/../.."
went to Nextcloud and failed there with an unclear error. The scope is
now built from the path's segments: empty and "." segments are dropped,
and ".." is refused with a clear error before any request is sent.

The scope stays unencoded on purpose. Nextcloud's search plugin takes a
relative scope href as a path without URL-decoding it, which the existing
integration test for a folder named "search #1 %41 Ü" relies on.

Signed-off-by: Björn Schotte <bjoern.schotte@googlemail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README and PROGRESS.md list the nine new tools (231 plus
upload_file_from_path), sync_progress.py maps the new calendar test
modules to the Calendar row, and the counts are recomputed with
scripts/sync_progress.py --write. Fixes the pyright error in the shared
calendar fixture's type.

Signed-off-by: Björn Schotte <bjoern.schotte@googlemail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a004c61f-9861-441a-9790-a563f5174588
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant