Skip to content

feat: add ntfy support#2769

Merged
enoch85 merged 2 commits into
Maintainerr:developmentfrom
GitGitro:ntfy-support
Apr 25, 2026
Merged

feat: add ntfy support#2769
enoch85 merged 2 commits into
Maintainerr:developmentfrom
GitGitro:ntfy-support

Conversation

@GitGitro

Copy link
Copy Markdown
Contributor

Note to contributors:
This template is part of our review process. Pull requests that do not meaningfully complete the sections below, or that do not demonstrate understanding of the change, may be closed without detailed review.

Description & Design

Please include:

  • What problem this change solves: added support for ntfy.sh as notification service
  • Why this approach was chosen: i followed the existing gotify agent and modified a bit to match ntfy format
  • How the solution works at a high level: user inserts url, topic name and auth token, see screenshot below
  • Any important trade-offs, edge cases, or limitations: none to my testing

Related issue

If this pull request addresses an issue, please link to it here (e.g., Fixes #123).

This one I guess: https://features.maintainerr.info/posts/101/add-ntfy-as-a-notification-agent

AI-Assisted Development

If you used AI tools (e.g. ChatGPT, Copilot, Claude) while working on this PR, please describe:

  • Which parts were AI-assisted: mainly the building of the payload for axios
  • What you personally reviewed, changed, or validated: every line of this pr
  • Why this solution fits this project’s coding standards and design: as i said above i just followed your gotify implementation across the project and adapted for ntfy

Note: The author is fully responsible for the correctness, testing, and maintainability of this change, regardless of tooling used.

Checklist

  • [ x ] I have read the CONTRIBUTING.md document.
  • [ x ] I understand the code I am submitting and can explain how it works
  • [ x ] I have performed a self-review of my code
  • [ x ] I have linted and formatted my code
  • [ x ] My changes generate no new warnings
  • [ x ] New and existing unit tests pass locally with my changes

How to test

Please describe the steps to test your changes, including any setup required.

  1. fork my branch
  2. yarn && yarn dev
  3. head to settings -> notifications and setup ntfy agent

Additional context

Hi!

sorry for not discussing this before but i had some spur of the moment and wanted to implement this today.
That said is very simple and readable i think. Yes as stated above I've used AI to help me write this but everything has been reviewed by me
Not much to be honest, tested and working on dev environment, let me know if there is something you'd like to change.
In the meantime I'll join the discord server so you can ping me there, and I'll a screenshot here

{7145FC96-BDEE-462E-BDBF-EAD32E87D0BA}

@GitGitro GitGitro requested a review from enoch85 as a code owner April 25, 2026 17:21
@enoch85

enoch85 commented Apr 25, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR! 🙏

Looks good, but I just need to double check before merging.

@enoch85 enoch85 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks close, but I think there’s one blocking issue with the ntfy integration.

In apps/server/src/modules/notifications/agents/ntfy.ts, apps/server/src/modules/notifications/agents/ntfy.ts, apps/server/src/modules/notifications/notifications-interfaces.ts, and apps/server/src/modules/notifications/notifications.service.ts, the implementation currently requires a token for every ntfy configuration. ntfy supports unauthenticated publishes to public topics, so this blocks a common valid setup. As written, users cannot save, test, or send notifications to a public topic unless they provide a token, which means the PR does not actually add general ntfy support.

I think the fix should be:

  1. Make token optional in the ntfy option type.
  2. Mark the token field as not required in the agent spec.
  3. Remove settings.options.token from shouldSend().
  4. Only send the Authorization header when a token is actually present.

I’m calling this out as a real regression against the stated scope because the PR description presents this as ntfy support generally, with no limitation that it only works for authenticated/private topics.

@GitGitro

Copy link
Copy Markdown
Contributor Author

I'm an idiot, I made the dev envirnoment identical as the prod I have and there everything is behind auth.

Thanks for the heads up I'll apply the changes you suggested tomorrow

@enoch85

enoch85 commented Apr 25, 2026

Copy link
Copy Markdown
Collaborator

@GitGitro No worries, I made the changes now. 👍

It will be in development soon.

@enoch85 enoch85 merged commit 83edbc2 into Maintainerr:development Apr 25, 2026
9 checks passed
@GitGitro

Copy link
Copy Markdown
Contributor Author

oh thank you very much! I appreciate it ❤️

maintainerr-automation Bot added a commit that referenced this pull request Apr 26, 2026
* ci(release): restore version header and add New Contributors section

Two gaps since we swapped @semantic-release/release-notes-generator for
the custom AI-backed tool:

- No top-level "# [X.Y.Z](compare-url) (YYYY-MM-DD)" header. Now
  prepended in all emit paths (model, fallback, empty).

- No "New Contributors" section. Now fetched from GitHub's
  POST /releases/generate-notes endpoint and appended if present.

* ci(fider): add automated triage, invitation, re-eval, and stale workflows

Five workflows under a unified Fider - * naming scheme that operate the
features.maintainerr.info Fider instance from CI. Tags + comments only —
no auto-closes from the triage path. Maintainers always have the final
call on possibly-* posts; the stale sweep is the one place the bot will
auto-decline (after a 30-day warning + grace window).

## Workflows

  fider-triage.yml              Fider - Triage              cron daily 06:00 UTC
  fider-re-evaluate.yml         Fider - Re-evaluate         cron monthly + manual
  fider-pre-existing-scan.yml   Fider - Pre-existing scan   manual only
  fider-stale.yml               Fider - Stale sweep         cron weekly Sunday
  fider-invite-codeowners.yml   Fider - Invite CODEOWNERS   cron weekly Sunday

## Tags applied by the bot

  triage-checked         every processed post (idempotency gate)
  possibly-completed     a recent merged PR clearly delivers the request
  possibly-duplicate     this post duplicates an earlier open post
  possibly-pre-existing  feature already shipped before the post was filed
                         (lower confidence than possibly-completed)
  stale                  open >1.5 years with <3 votes; warned for review

## Triage pipeline (per post)

1. Skip if already triage-checked, unless FORCE_REEVAL=true
2. Extract keywords from title + first 400 chars of description, drop
   stopwords, keep top 4
3. Search merged PRs one keyword at a time via gh pr list, union and
   dedupe (AND'ing keywords misses real matches)
4. Filter candidates: feat/fix/feature prefix only, mergedAt > createdAt
5. Ask the model (openai/gpt-4.1-mini) with a strict JSON rubric that
   requires a verbatim quote from a candidate PR; verify the quote
   actually appears in what we sent (defence against hallucination)
6. If status=completed + confidence=high: tag possibly-completed and
   upsert a comment citing the evidence URL and verbatim quote (PUT
   to update on re-eval if a better PR is now cited)
7. (CHECK_PRE_EXISTING=true) Re-run the search with mergedAt < createdAt
   and a stricter rubric to surface "user didn't know feature exists"
8. Compare against other open posts (Jaccard >= 0.4 on keywords) for
   duplicate detection; the cited original_number must be in the
   candidate list AND the quote must appear in its title/description

## Stale sweep (two-phase)

Phase 1: post older than 1.5 years with <3 votes and no exempt tag →
         apply 'stale' tag, post a "still relevant?" warn comment
         carrying the same cc:@maintainers prefix as triage comments
Phase 2: 30 days later, if no engagement (any comment newer than the
         warn) → PUT /api/v1/posts/{n}/status to declined with
         explanation
Exempt tags: never-stale, planned, started, bug, enhancement,
possibly-completed

## Invite-CODEOWNERS sweep

Weekly: parse .github/CODEOWNERS, look up each user's public GitHub
email, skip those already Collaborator/Administrator on Fider, send
remaining recipients a single POST /api/v1/invitations/send. Users
with private email visibility are logged and skipped (no automatic
fallback — invite manually if needed).

## Safety guards (triage)

- Per-comment-type hidden HTML markers prevent double-commenting on
  re-runs (completed / duplicate / pre-existing each independent);
  upsertBotComment edits in place if the body changed
- 5-second throttle between model calls (free-tier 15 req/min cap)
- Per-call retry on 429/5xx: 60s/120s/240s backoff, honour Retry-After
  but cap at 5 minutes — anything beyond (e.g. 23-hour daily quota
  reset) becomes a hard failure so the run aborts cleanly instead of
  sleeping inside CI
- Abort run after 3 consecutive model failures (~15s on quota-exhausted
  days, ~21min worst case during real outages) — remaining posts get
  picked up on the next scheduled run
- Rate-limit headers logged via an explicit allowlist; trace IDs and
  infra identifiers are deliberately NOT logged
- CODEOWNERS guard on workflow_dispatch
- models: read permission only; no actions write, repo write, PR write

## Maintainer @mentions on every bot comment

Resolves a "cc:" prefix once per run from GET /api/v1/users (works for
Collaborator OR Administrator per docs.fider.io/api/users) or falls
back to FIDER_MENTION_USERS_FALLBACK env var. The bot's own user is
auto-excluded via FIDER_BOT_USERNAME (no /me endpoint exists in
Fider's public API). If neither source yields anyone, the comment
posts without the cc line.

## Shared helpers

tools/fider-shared.mjs provides createFider, postHasTag,
buildMentionToken, and buildMentionPrefix to all four entry-point
scripts so the API client, role filtering, and cc-prefix logic stay
in one place.

## Bot identity

  GitHub user:  maintainerr-fider-bot (Read collaborator on the repo)
  Fider role:   Collaborator (with one-time Administrator promotion
                to create the five private tags)
  Secret:       FIDER_API_KEY (personal access token)

* fix(fider-stale): create the 'stale' tag on first run + share ensureTags

The first live stale-sweep run failed with 404 on tag application
because the 'stale' tag didn't exist on Fider yet — the script never
called the equivalent of fider-triage's ensureTags(). Both scripts
now use a single shared helper:

- New export ensureTags({ fider, log, dryRun, host, tags }) in
  tools/fider-shared.mjs that idempotently creates a list of tags
  and converts the 403-on-Collaborator failure into the same
  promote-then-demote instructional error message both scripts
  used to inline.
- fider-stale.mjs now calls it for the 'stale' tag (color e74c3c).
- fider-triage.mjs's local ensureTags shrinks from 36 lines to a
  10-line invocation passing its four tag definitions.

Net: -23 lines despite adding tag creation to fider-stale.

* feat(fider): replace cc:@user comment prefix with Discord notifications

The cc: prefix was cosmetic — Fider doesn't send notifications on
@mentions, so maintainers never saw the bot's output until they opened
the post directly. Two visible bugs in the live runs (bot included
itself as @maintainerr-fider-bot, ydkmlt84 listed twice because of
two Fider accounts under the same display name) were also confusing.

Replace it with a Discord webhook integration.

## What changed

- New notifyDiscord helper in fider-shared.mjs. Posts a coloured embed
  to a webhook URL on each meaningful bot action: possibly-completed,
  possibly-pre-existing, possibly-duplicate, stale-warned, and
  stale-declined. Embed contains the post title (clickable), the cited
  PR / quote / vote count, and a colour matching the Fider tag.
- Optional DISCORD_PING_ROLE_ID — when set, the message also includes
  <@&ROLE_ID> in the content field so a Discord role gets pinged
  (embeds alone never trigger Discord notifications). allowed_mentions
  is scoped to that single role so it can never accidentally @everyone.
- All buildMentionPrefix / withMentionPrefix / FIDER_BOT_USERNAME /
  FIDER_MENTION_USERS_FALLBACK code removed from triage and stale
  scripts. Comments now post without a cc line.
- buildMentionPrefix and buildMentionToken removed from
  tools/fider-shared.mjs (they were the only consumers).

## Workflow plumbing

All four runtime workflows (fider-triage, fider-re-evaluate,
fider-pre-existing-scan, fider-stale) pass two new env vars:
  DISCORD_FIDER_BOT_WEBHOOK  ← repo secret
  DISCORD_PING_ROLE_ID       ← repo secret (treated as sensitive too)
Both default to empty: webhook empty = no notifications,
role ID empty = embed-only with no @-ping. The bot's Fider work always
happens; Discord is best-effort and never throws on failure.

## Setup required by maintainers

1. Create a Discord webhook in the target channel
   (Server Settings → Integrations → Webhooks → New Webhook),
   copy the URL, store as repo secret DISCORD_FIDER_BOT_WEBHOOK.
2. (Optional) Create or pick a "maintainers" role in the Discord
   server, copy its numeric snowflake ID (Developer Mode →
   right-click role → Copy ID), store as repo secret
   DISCORD_PING_ROLE_ID. Members of that role get pinged on every
   bot action.

* feat: add ntfy support (#2769)

* fix: stop cross-rule contamination for same-titled automatic collections (#2766)

* fix: stop cross-rule contamination for same-titled automatic collections

Two automatic rule groups with the same title end up linked to the same
media server collection because checkAutomaticMediaServerLink relinks by
name. The rule executor's manual-import path then imported the sibling
rule's items as manual into the wrong rule, subjecting them to the
wrong deleteAfterDays. Whichever rule emptied first would also delete
the shared media server collection in removeFromCollection /
checkAutomaticMediaServerLink, taking the sibling rule's items with it.

Skip the manual import for items already rule-owned by a sibling
collection sharing the same mediaServerId, and guard the two empty-
collection deletes against shared links by unlinking locally instead.

Refs #2765

* fix: resync rule-owned items + close sibling-lookup gaps

Adds three corrections on top of the same-title automatic collection
contamination fix:

- Partial drift: checkAutomaticMediaServerLink now diffs server children
  against local rule-owned items for shared automatic collections (not
  just empty ones) and pushes any missing back via addBatchToCollection.
  Fixes the "Plex shows 1 of 5 items, Maintainerr DB shows 5" case where
  the rule executor's local-DB-only delta can't recover server drift.
- Sibling lookup error path: getSiblingRuleOwnedMediaServerIds no longer
  swallows DB errors into an empty Set. The rule executor catches the
  throw and skips manual child import, since a silent empty would let
  sibling-owned children be imported as manual into the wrong rule.
- Cross-type sharing: isMediaServerCollectionShared and
  getSiblingRuleOwnedMediaServerIds now filter siblings by
  manualCollection so a manual collection sharing a mediaServerId can no
  longer be treated as a sibling automatic rule group.

* ci(docs-drift): flag user-facing fix commits and default to latest release

- Adds a "Behavioral fixes worth reviewing" section that scans fix:
  commits and surfaces the ones touching user-visible surfaces (UI,
  settings, notifications, collections, rule executor, controllers,
  README) so behavior-changing fixes don't slip past the drift report.
  Each entry lists the touched files for cheap triage.
- workflow_dispatch base_ref is now a small choice dropdown (latest /
  origin/main / origin/development) defaulting to 'latest', which the
  resolve step expands to the most recent GitHub release tag at run
  time. No per-version maintenance burden.

* fix(notifications): dedupe sibling-rule media events within a batch

When two automatic rule groups share a media server collection (same
title in the same library), each emits its own CollectionMedia_Added /
_Removed for the same item, so the user sees N identical notifications
for what is one user-visible change. Dedupe these within a single
rule-executor batch (one processQueue() pass), keyed on
(event, collectionName, mediaServerId).

Notifications across separate batches (e.g. scheduled runs at different
times) are never collapsed — those are genuinely distinct events.
Manual / user-triggered notifications are unaffected because dedupe
lives in the @onevent handlers, not in handleNotification itself.

Reuses the existing RuleHandlerQueue_StatusUpdated event for batch
lifecycle so no new contracts surface is added. Adds a defensive
emitStatusUpdate() to processQueue's outer finally so listeners can
observe the processingQueue=true→false transition reliably (previously
no event fired on drain). Verified safe under stop/abort: the same
finally path runs and clears dedupe state.

* chore: add workspace MCP configuration

---------

Co-authored-by: maintainerr-automation[bot] <261505141+maintainerr-automation[bot]@users.noreply.github.com>
Co-authored-by: enoch85 <mailto@danielhansson.nu>
Co-authored-by: Gitro <108683123+GitGitro@users.noreply.github.com>
@enoch85 enoch85 added this to the 3.9.0 milestone Apr 28, 2026 — with GitHub Codespaces
@maintainerr-automation

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.9.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants