Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions .github/workflows/analysis-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ env:
APP_PORT: 9009
NC_AUTH_USER: "admin"
NC_AUTH_PASS: "adminpassword"
NEXTCLOUD_BRANCH: stable31
# The OCI job uses the EOL oracle-xe:11 image, which cannot resolve the Etc/UTC
# session timezone that NC >= 34 sets (ORA-01882). Keep this job on stable33 until
# the Oracle image is bumped to 18+.
NEXTCLOUD_BRANCH: stable33

jobs:
analysis:
Expand All @@ -42,7 +45,7 @@ jobs:
tests-oci:
needs: [analysis]
runs-on: ubuntu-22.04
name: stable31 • 🐍3.11 • OCI
name: stable33 • 🐍3.11 • OCI
timeout-minutes: 60

services:
Expand Down Expand Up @@ -204,7 +207,7 @@ jobs:
with:
repository: nextcloud/notes
# Notes main (v5.0.0) requires NC >= 33; pin older servers to the last 4.x (supports NC 28-34).
ref: "${{ (matrix.nextcloud == 'stable31' || matrix.nextcloud == 'stable32') && 'v4.13.1' || 'main' }}"
ref: "${{ matrix.nextcloud == 'stable32' && 'v4.13.1' || 'main' }}"
path: apps/notes

- name: Checkout Files Locking
Expand Down Expand Up @@ -338,7 +341,7 @@ jobs:
strategy:
fail-fast: false
matrix:
nextcloud: [ 'stable31', 'stable32' ]
nextcloud: [ 'stable32', 'stable34' ]
Comment thread
oleksandr-nc marked this conversation as resolved.
env:
NC_dbname: nextcloud_abz
DATABASE_PGSQL: 1
Expand Down Expand Up @@ -384,6 +387,16 @@ jobs:
ref: ${{ matrix.nextcloud }}
path: apps/notifications

- name: Build Notifications if it ships an unbuilt Composer autoloader
working-directory: apps/notifications
# Since stable34 the app depends on web-push and its composer/autoload.php
# requires vendor/autoload.php, which a plain git checkout does not include.
# Older branches load without it, so only build when the checkout needs it.
run: |
if grep -q "vendor/autoload.php" composer/autoload.php 2>/dev/null && [ ! -f vendor/autoload.php ]; then
composer install --no-dev --ignore-platform-reqs
fi

- name: Checkout Activity
uses: actions/checkout@v6
with:
Expand Down Expand Up @@ -515,7 +528,7 @@ jobs:
strategy:
fail-fast: false
matrix:
nextcloud: [ 'stable31', 'stable33' ]
nextcloud: [ 'stable33', 'stable34' ]
Comment thread
oleksandr-nc marked this conversation as resolved.
env:
NEXTCLOUD_URL: "http://localhost:8080/index.php"
timeout-minutes: 60
Expand Down Expand Up @@ -549,8 +562,8 @@ jobs:
uses: actions/checkout@v6
with:
repository: nextcloud/notes
# Notes main (v5.0.0) requires NC >= 33; pin older servers to the last 4.x (supports NC 28-34).
ref: "${{ (matrix.nextcloud == 'stable31' || matrix.nextcloud == 'stable32') && 'v4.13.1' || 'main' }}"
# This job only runs NC >= 33, so Notes main (v5.0.0) is always compatible.
ref: main
path: apps/notes

- name: Checkout Files Locking
Expand Down
6 changes: 4 additions & 2 deletions nc_py_api/nextcloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,8 @@ def app_cfg(self) -> AppConfig:
def register_talk_bot(self, callback_url: str, display_name: str, description: str = "") -> tuple[str, str]:
"""Registers Talk BOT.

.. note:: AppAPI will add a record in a case of successful registration to the ``appconfig_ex`` table.
.. note:: On Nextcloud <= 33 AppAPI mirrors the secret into ``appconfig_ex``; since 34 it does not, so
``TalkBot.enabled_handler`` persists its own copy there under a separate key for other workers.

:param callback_url: URL suffix for fetching new messages. MUST be ``UNIQ`` for each bot the app provides.
:param display_name: The name under which the messages will be posted.
Expand Down Expand Up @@ -524,7 +525,8 @@ def app_cfg(self) -> AppConfig:
async def register_talk_bot(self, callback_url: str, display_name: str, description: str = "") -> tuple[str, str]:
"""Registers Talk BOT.

.. note:: AppAPI will add a record in a case of successful registration to the ``appconfig_ex`` table.
.. note:: On Nextcloud <= 33 AppAPI mirrors the secret into ``appconfig_ex``; since 34 it does not, so
``TalkBot.enabled_handler`` persists its own copy there under a separate key for other workers.

:param callback_url: URL suffix for fetching new messages. MUST be ``UNIQ`` for each bot the app provides.
:param display_name: The name under which the messages will be posted.
Expand Down
27 changes: 25 additions & 2 deletions nc_py_api/talk_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ def enabled_handler(self, enabled: bool, nc: NextcloudApp) -> None:
if enabled:
bot_id, bot_secret = nc.register_talk_bot(self.callback_url, self.display_name, self.description)
os.environ[bot_id] = bot_secret
# Persist it so any worker (not only the one that registered) can sign requests. Since
# AppAPI 34 the secret is no longer mirrored into ``appconfig_ex`` for us to read back.
nc.appconfig_ex.set_value(_bot_secret_config_key(bot_id), bot_secret, sensitive=True)
else:
nc.unregister_talk_bot(self.callback_url)

Expand Down Expand Up @@ -251,6 +254,9 @@ async def enabled_handler(self, enabled: bool, nc: AsyncNextcloudApp) -> None:
if enabled:
bot_id, bot_secret = await nc.register_talk_bot(self.callback_url, self.display_name, self.description)
os.environ[bot_id] = bot_secret
# Persist it so any worker (not only the one that registered) can sign requests. Since
# AppAPI 34 the secret is no longer mirrored into ``appconfig_ex`` for us to read back.
await nc.appconfig_ex.set_value(_bot_secret_config_key(bot_id), bot_secret, sensitive=True)
else:
await nc.unregister_talk_bot(self.callback_url)

Expand Down Expand Up @@ -356,12 +362,25 @@ def __get_bot_secret(callback_url: str) -> str:
return sha_1.hexdigest()


def _bot_secret_config_key(bot_id: str) -> str:
# nc_py_api-owned ``appconfig_ex`` key used to persist the secret for other workers/processes.
# Kept distinct from ``bot_id``: AppAPI <= 33 stores and re-reads the secret under ``bot_id``, so
# writing there ourselves would corrupt its bookkeeping (AppAPI 34+ does not store it at all).
sha_1 = hashlib.sha1(usedforsecurity=False)
sha_1.update((bot_id + "_ncpyapi_secret").encode("UTF-8"))
return sha_1.hexdigest()


def get_bot_secret(callback_url: str) -> bytes | None:
"""Returns the bot's secret from an environment variable or from the application's configuration on the server."""
secret_key = __get_bot_secret(callback_url)
if secret_key in os.environ:
return os.environ[secret_key].encode("UTF-8")
secret_value = NextcloudApp().appconfig_ex.get_value(secret_key)
appcfg = NextcloudApp().appconfig_ex
# AppAPI <= 33 mirrors the secret under ``secret_key``; AppAPI 34+ does not, so also read our own copy.
secret_value = appcfg.get_value(secret_key)
if secret_value is None:
secret_value = appcfg.get_value(_bot_secret_config_key(secret_key))
if secret_value is not None:
os.environ[secret_key] = secret_value
return secret_value.encode("UTF-8")
Expand All @@ -373,7 +392,11 @@ async def aget_bot_secret(callback_url: str) -> bytes | None:
secret_key = __get_bot_secret(callback_url)
if secret_key in os.environ:
return os.environ[secret_key].encode("UTF-8")
secret_value = await AsyncNextcloudApp().appconfig_ex.get_value(secret_key)
appcfg = AsyncNextcloudApp().appconfig_ex
# AppAPI <= 33 mirrors the secret under ``secret_key``; AppAPI 34+ does not, so also read our own copy.
secret_value = await appcfg.get_value(secret_key)
if secret_value is None:
secret_value = await appcfg.get_value(_bot_secret_config_key(secret_key))
if secret_value is not None:
os.environ[secret_key] = secret_value
return secret_value.encode("UTF-8")
Expand Down
7 changes: 7 additions & 0 deletions tests/actual_tests/teams_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,13 @@ async def test_teams_personal_circle(anc_any):


@pytest.mark.asyncio(scope="session")
@pytest.mark.xfail(
raises=NextcloudException,
reason="Upstream Circles regression: creating a local circle through an ExApp (app mode) returns HTTP 400 "
"since the nextcloud/circles local-controller permission changes (mid-2026). Client mode is unaffected. "
"Remove this marker once Circles is fixed on the stable branches.",
strict=False,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
async def test_teams_local_circle(anc_any):
if await anc_any.teams.available is False:
pytest.skip("Teams (Circles) is not installed")
Expand Down