Previously, the events finder could filter events by whether they had a
URL, a location, or both, exposed as the `event_format` parameter added
in #43131.
We are still figuring how should we define the event_format
Previously, post events could identify their creator but could not
assign event hosts.
This change persists, validates, and serializes hosts as a first-class
post-event attribute.
Previously, the event finder could not filter by tags, search text,
status, or format, and malformed date or limit parameters could fail
unpredictably. ( `?after=foobar` / `?before=2025-13-45` / `?after=now`).
This change adds those generic filters, a lightweight event-card
response, and consistent validation for date and limit parameters.
Previously, the create-topic button's click always opened the composer,
so a plugin that needs its own creation flow (for example, an event form
in event categories) had to hide the core button with a CSS hack and
render a second button next to it, losing the drafts menu along the way.
This change wraps the click in a `create-topic-button-click` behavior
transformer with the current category and tag as context, so a plugin
can take over the click (or call `next()` to keep the default) while
core's button, its label and icon transformers, and the drafts menu stay
in place.
Follow-up to #42799 (stacked on it), doing for tags what that PR did for
categories. The admin assistant's prompt says it helps with "tags", but
its only tag write tool applies tags to a topic — it cannot create a tag
ahead of time, rename one, or set a tag description.
- **`create_tag`** — creates a standalone tag with an optional
description, normalizing the name through `DiscourseTagging.clean_tag`
just like manual creation, and logs a `created_tag` staff action.
- **`edit_tag`** — renames an existing tag and/or changes its
description (`can_edit_tag?`), logging the same `renamed_tag` staff
action the tags controller writes.
- **`edit_tags` description fix** — the signature claimed "Adds tags to
a topic. By default appends", but with `replace: true` it replaces the
full tag set and removes omitted tags. The signature and `tags`
parameter now say so; behavior is unchanged.
Both new tools require human approval and attribute the action to the
approving moderator, matching the category tools. They sit inside the
`tagging_enabled` gate in `all_available_tools`, so they disappear
cleanly on sites without tagging.
The admin assistant (Discourse Helper Bot) claims it can set up
categories when asked, then fails: its only category write tool,
`edit_category`, despite the name, could only move a topic to a
different category. Admins asking "create these categories for me" got a
capable-sounding yes followed by a refusal.
This adds real category management:
- **`create_category`** — creates a category (or subcategory via
`parent_category_id`) with optional description and colors. Returns the
new category's id and URL so the model can create parents before
subcategories across turns.
- **`edit_category`** — repurposed to do what its name says: edit an
existing category's name, description (revising the definition topic
through the standard `Category#update` path), and colors.
- **`change_topic_category`** — the previous `edit_category` behavior
(recategorizing a topic via `PostRevisor`), preserved under a name that
matches what it does.
All three require human approval before executing and are attributed to
the approving moderator (`attribute_to_approver?`), so guardian checks
and staff action logs (`create_category`, `change_category_settings`)
credit the real approver rather than the bot account. The new tools are
added to the admin assistant persona and `all_available_tools`; the seed
fixture syncs them to existing sites on migrate. A data migration
renames stored `EditCategory` entries in `ai_agents.tools` and pending
`ai_tool_actions` rows to `change_topic_category`, so existing agents
and in-flight approvals keep working.
Previously, staff could not identify which AI triage automation sent an
item to the review queue using the existing Type and Reason filters.
This change extends `add_custom_reviewable_filter` with optional Type
and Reason filter aliases, showing AI triage and current automation
names while matching stable automation IDs stored on reviewable scores.
With this, you can filter by the automation name in the `reason` filter:
<img width="1930" height="1036" alt="image"
src="https://github.com/user-attachments/assets/5095564b-249c-46de-89bb-0036dd28577c"
/>
I have an automation that flags any post with the word `silly`
When a topic is marked as spam and deleted, the existing `/delete`
MessageBus update marks it as deleted in the client topic tracking
state. Deleted states were still eligible for new and unread counts,
though, and the topic could remain in the incoming-topic list.
This change makes the existing new/unread predicates ignore deleted
topics and clears a deleted topic from the current incoming list. No new
server-side correction message is needed.
The regression is covered by the topic tracking state unit tests and a
browser-level spam-deletion spec.
Before:
<img width="1400" height="1400" alt="Category new-topic count remains
after spam deletion"
src="https://github.com/user-attachments/assets/057a0a77-0113-4cf4-8bf8-5b645090cdcb"
/>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, 361869df7c stopped serializing `allowed_tag_groups` for
users who cannot edit the category. This went further than hiding
restricted names: it also removed the names of tag groups that are
visible to everyone — the same names any user (including anonymous) can
already enumerate through `/tag_groups/filter/search` — which broke
per-category tag-group UIs for everyone except editors.
This change serializes `allowed_tag_groups` on the full category record
for all users, filtered through `TagGroup.visible(guardian)` — the same
visibility filter the public search endpoint applies — so restricted
group names remain hidden from non-editors. Editors keep the unfiltered
list, which the category edit UI round-trips.
Note for reviewers: the one piece of information this exposes that was
not previously readable by non-editors is the mapping of visible tag
groups to a specific category (the group names themselves are already
public via the search endpoint, and the composer's tag chooser already
returns a required group's name to any user who can post). Reported by a
site running a per-category tag-group filter UI, where the field's
removal made the filters render for staff only.
When an LLM triage automation flags a post, the reviewable's score
reason says:
> **Triggered by the [My automation](/admin/plugins/automation/1)
rule.**
That link is broken — it leads to a page that does not exist.
The automation admin UI is nested under the plugin show route
(`/admin/plugins/:plugin_id`), and registers its own `automation` child
route. So its edit page lives at
`/admin/plugins/automation/automation/:id` — the plugin id, then the
nested route. The link only had one of the two segments.
This was originally broken when automation moved to the new show route
and the URL changed from `/admin/plugins/discourse-automation/:id`.
#35434 dropped the `discourse-` prefix but missed the extra segment, so
the link stayed broken.
Reported by a customer who landed on `/admin/plugins/automation/1` from
their review queue.
The translated locale files carry the same URL but are managed by
Crowdin, so they are left to re-sync from `en`.
Previously, the admin UI hid the delete button for seeded system badges,
but `DELETE /admin/badges/:id` accepted direct requests and destroyed
the badge, its `UserBadge` grants, and any user titles granted from it.
This change rejects the request with a translated 422 error before
anything is logged or destroyed — matching how system flags and
automatic groups are protected — while leaving custom badge deletion
unchanged.
Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
Previously, the `themes-with-setting` endpoint reported themes with no
`ThemeSiteSetting` override as disabled, even though a missing override
means the theme inherits the `enable_welcome_banner` default (true), so
saving unrelated fields on the admin welcome-banner form silently
persisted `false` and hid the banner for those themes.
This change falls back to `SiteSetting.defaults[:enable_welcome_banner]`
only when no override row exists — while still reporting explicit
`false` overrides correctly — matching how
`ThemeSiteSetting.generate_theme_map` and `ThemeSiteSettingResolver`
resolve effective values.
## Source
- Patch Triage: https://patch.discourse.org/patch-triage/1666
Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
Pluralized email-template subject and body values are hashes. When an
invalid edit triggered the rollback path, it attempted to store that
hash as a translation override and raised a TypeError.
Track the original override state for scalar values, restore it after a
failed update, and leave pluralized roots untouched. Invalid edits now
return the expected 422 response without creating override records.
Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
Previously, approval-required Admin Assistant tools chained into another
model response, which could replace the interactive approval card with a
non-interactive pending-approval summary.
This change adds a default-off `stop_chain_on_pending_approval?` agent
capability, enables it for the Admin Assistant, processes every tool
call in the current response, and then leaves the approval cards as the
authoritative result.
Previously, staff could persist user notes longer than the configured
maximum post length, which could consume excessive client resources when
rendered.
This change rejects user notes longer than `SiteSetting.max_post_length`
before persistence.
Relates to https://patch.discourse.org/patch-triage/1360
Previously, the profile feature-topic endpoint returned different
responses for missing topics and existing topics the requester could not
access, revealing whether a topic record existed.
This change checks topic visibility before feature eligibility,
returning a not-found response for both missing and inaccessible topics.
Relates to this patch: https://patch.discourse.org/patch-triage/1350
Previously, AI agents could look up site settings (`search_settings`,
`setting_context`) but had no way to change them.
This change adds a `change_site_setting` tool that requires admin
approval before anything is applied: the proposed change shows up as an
inline review card in the bot conversation, and on approval it is
applied through the `SiteSetting::Update` service — keeping all of its
guardrails (no hidden, deprecated, globally shadowed, or unconfigurable
settings) — credited to the approving admin in the staff action log.
## Demo:
https://github.com/user-attachments/assets/6782d8f4-0182-47dc-92b5-1db77b7a1ee1
---------
Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
> Was stacked on #41497, which has since merged. This PR is now rebased
onto `main` and contains only the chat-approval work.
### What
Lets a moderator approve or reject an AI-bot moderation tool action
(`suspend_user` / `silence_user`) **inside a Chat direct message with
the bot**, instead of leaving for the `/review` queue — the chat
counterpart to the base PR's inline PM/topic card.
When the bot queues one of these actions in a DM, it posts a message
with **Approve / Reject** buttons rendered via the Chat plugin's native
interactive **blocks**. Clicking performs the queued action through the
existing `ReviewableAiToolAction` backend (credited to the approving
moderator) and rewrites the message to its resolved state, removing the
buttons.
### How
- `bot.rb` — `enqueue_tool_for_approval` branches on chat context: in
chat it emits a `:chat_approval` signal; in PM/topic it keeps the
existing inline card.
- `playground.rb` — `reply_to_chat_message` posts a bot chat message
carrying the Approve/Reject blocks, in the **same DM thread as the bot's
reply** (AI-bot DM replies are threaded by design). DM channels only.
- `chat_tool_approval.rb` — builds/parses the button `action_id`s,
builds the blocks, and handles the `chat_message_interaction` event:
performs the reviewable and rewrites the message. Runs synchronously so
the buttons clear before the request returns.
- `entry_point.rb` — registers the `:chat_message_interaction` listener.
### Authorization
`Chat::CreateMessageInteraction` only checks channel visibility, so
staff-gating is enforced here: the handler requires
`Reviewable.viewable_by(user)` **and** `Reviewable#perform` re-checks
(`ensure_performed_by_is_a_real_person!` + the approver's guardian).
Non-staff clicks are ignored. A crafted `action_id` can't target another
reviewable — core only matches `action_id`s present in that message's
own blocks.
### Core-chat changes (3 lines)
The blocks system was built for **create-time-only** blocks; nothing had
ever mutated a message's `blocks` after creation. Clearing the buttons
on approve/reject is the first such case, which required:
- `chat-message.js` — make `blocks` a `@tracked` property (so
reassigning it re-renders).
- `chat-channel-subscription-manager.js` +
`chat-channel-thread-subscription-manager.js` — refresh `message.blocks`
in `handleEditMessage` (so the block-clearing edit reaches the client,
in both the channel and thread views).
All are no-ops for the only other block user (category blocks, which are
never edited after creation).
### Testing
`plugins/discourse-ai/spec/lib/ai_bot/chat_tool_approval_spec.rb` —
action-id round-trip, block shape, staff gating, foreign/stale
action-ids, approve/reject, failure surfacing, and an end-to-end run
through the real `Chat::CreateMessageInteraction` service. The existing
`playground_spec.rb` chat-DM tests (threaded conversation + context)
continue to pass. Verified manually in a bot DM.
2026-07-13 17:08:35 -03:00
Gabriel Grubbaanddiscourse-patch-triage <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
Full-text search (`Search#tags_search`, which powers the anonymous
`/search/query.json` endpoint) filtered matching tags through
`DiscourseTagging.hidden_tag_names`. That helper only accounts for
tag-group permissions, so tags restricted to a private category via
`CategoryTag` or `CategoryTagGroup` were never filtered out. An
anonymous or unauthorized user could search for a term and receive the
restricted tag's name, slug, and description (including localized
descriptions) in the `tags` array, even with no access to the category
itself. Because the search term can be swept, this allowed enumeration
of otherwise-hidden tag metadata.
This came from Patch Triage:
https://patch.discourse.org/patch-triage/1434
Co-authored-by: discourse-patch-triage <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
See demo:
https://github.com/user-attachments/assets/930ce466-d859-42e2-af93-d6255aa942ce
Now these pages:
- admin/email-logs
- admin/email/templates
- admin/config/customize/components
- admin/config/colors
- admin/badges
- admin/reports
- admin/config/upcoming-changes
Support URL state for their filter controls. This means that you can now
share links to these pages with the current filter state preserved in
the URL.
Previously, AI agents had no way to suspend or silence users, and
approving any moderation tool action meant leaving the conversation for
the `/review` queue.
This change adds `suspend_user`/`silence_user` tools that always require
moderator approval, and lets moderators approve or reject those actions
from an inline review card right in the bot conversation — credited to
the approving moderator (not the bot) in the staff action log — instead
of switching to the `/review` queue.
Demo of this feature:
https://github.com/user-attachments/assets/b9605511-e53d-483d-b435-b212694cdf53https://github.com/user-attachments/assets/383bda5d-93ee-4758-a51e-6162ea0d57ee
---------
Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
Admins can now search for multiple users at once in `/admin/users/list`
by
separating usernames or emails with commas or whitespace — useful for
support workflows where a list of users needs to be checked in one go
(e.g. pasted from a spreadsheet; spaces work because usernames and
emails
can never contain them).
The search is also shareable via URL: typing keeps the address bar in
sync
with a `filter` query param
(`/admin/users/list/active?filter=sam,bob@example.com`),
and visiting such a URL prefills the search box and filters the list.
The
old `username` query param keeps working as a read-only legacy fallback
—
it was a misleading name, since searches also match emails.
## Demo:
https://github.com/user-attachments/assets/c85e4f30-3c06-493b-9fed-bb2057209c5d
The category list aggregate topic counts included direct subcategories
without
checking whether the current guardian could see them.
Before we the only possible information that could be gotten was:
- private subcategory topic counts
- recency buckets for topic creation: day/week/month/year/all-time
Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
Public account creation accepted `title` from non-admin signup requests,
allowing a new user to assign themselves an arbitrary visible profile
title before normal title authorization checks could apply.
Ignore `title` for non-admin account creation, matching the existing
treatment of primary/flair group parameters. Admin API user creation
remains unchanged.
Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
Add `ensure_can_see_group` to places where we needed it before returning
the group metadata.
Also added
`.visible_groups(@guardian.user).members_visible_groups(@guardian.user)`
to guard `advanced_filter` search
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:57:46 -03:00
Gabriel Grubbadiscourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>Gerhard Schlager
## Summary
ZIP symlink entries are extracted as real directories – we should verify
before extracting
## Source
- Patch Triage: https://patch.discourse.org/patch-triage/890
---
🤖 Auto-generated from the patch diff via Patch Triage. Review carefully
before merging.
Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
---------
Co-authored-by: discourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
Co-authored-by: Gerhard Schlager <gerhard.schlager@discourse.org>
When a user is removed from a PM, their existing notifications for that
topic weren't cleaned up, so they lingered in the user menu and still
showed the PM title. Nothing beyond the title was exposed(which they had
access to at some point), and removed users don't receive notifications
for new messages.
Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
We added this in 2016, and at the time, staff could add `<div
style="...">`.
If users want to add styles, they should use themes/theme components.
Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
Previously, `Tags::Search` looked up conflicting one-per-topic tag names
with an unscoped `Tag` query, allowing users to disclose hidden tag
names by passing their IDs in `selected_tag_ids`.
This change scopes that lookup to `visible_tags` and returns a generic
conflict reason when no conflicting selected tags are visible, so hidden
tag names are no longer exposed.
Reported in patch-triage/1124
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Before, a user who left a direct message or lost access to a private
category channel could still delete their own messages there, because
`can_delete_chat?` only checked ownership and channel status — not
current visibility.
This change requires `can_preview_chat_channel?` for self-deletes,
mirroring `can_restore_chat?`, so former members can no longer trash
messages in channels they can no longer access.
This ensures symmetry in guardian methods.
Relates to PATCH-1087
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`reactions_received` built its post list with
`filter_allowed_categories`, which only scopes by category. We should be
checking it as we would for a regular post list
With this change, this method is in line with `reactions_given`, which
already runs its results through a visibility guard.
relates to: PATCH-1292
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Follow-up to #39835, which converted the post-event builder to FormKit.
The FormKit `image` control expects its field value to be a URL string:
it forwards the value straight to `UppyImageUploader`, which runs it
through `getURLWithCDN`. The advanced builder instead stored the whole
upload object as the field value, so the control handed an object to
`getURLWithCDN` and threw `startsWith is not a function`.
This was erroring on deployed sites, but on locally it was working as
normal.
Store the URL string in the form field (what the image control needs)
while keeping the upload object on the event model (what `buildParams`
needs for the `[event image="…"]` short_url). Everything else behaves as
before.
---
Also updated the modal to use `@inline` so it can be mounted in a
rendering test; it is `undefined` in regular run mode (modals open via
`modal.show`), so there is no behaviour change.
Demo before:
https://github.com/user-attachments/assets/9fd7fe7c-b317-4192-9256-7f46d11b9859
Demo now:
https://github.com/user-attachments/assets/eb9ed34f-a0a5-43b1-abd5-2ca6ea8c8e11
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to #40549, which added a `can_see_topic?` guard to
`UserCardSerializer`
`CurrentUserSerializer` inherits from `BasicUserSerializer` directly, so
it kept its own unguarded `featured_topic` – this PR fixes that
relates to patch/1278
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This is a revival of this PR:
https://github.com/discourse/discourse/pull/33636
### Description
This PR adds ReCaptcha as a Captcha provider and provides a refactor to
allow other Captcha providers to be added more easily. To achieve this,
the following changes were introduced:
- Provider Pattern Introduction in
lib/discourse_hcaptcha/captcha_provider.rb
- Controller refactoring, created a new Parent controller, extracted
common logic
- Serializer for each captcha provider
- Implemented base abstract component for FE captcha logic
- h-captcha-service is now provider agnostic (captcha-service) and
manages both captcha providers
- Added connectors to the UI to allow the chosen captcha to be rendered
- Added problem check and site settings for the newly introduced
ReCaptcha
- `before-create-account` valueTransformer is introduced to allow
captcha validation (or any other data) without modifying the class.
---------
Co-authored-by: Juan Martinez <juan@discourse.org>
Co-authored-by: David Taylor <david@taylorhq.com>
Ranked-choice polls always serialized `ranked_choice_outcome` (the
computed winner and round-by-round activity) regardless of the poll's
`results` setting, unlike vote counts and voter lists, which are gated
by `Poll#can_see_results?`.
The outcome was therefore displayed to users who should not yet see
results on `on_vote`, `on_close`, and `staff_only` polls — through the
topic view, the vote/remove-vote responses, and the MessageBus
broadcast.
- Gate `ranked_choice_outcome` in `PollSerializer` behind
`can_see_results?`, matching the other result fields.
- Stop manually re-appending the outcome in `vote`/`remove_vote` so the
serializer's gating is authoritative.
- Serialize the MessageBus payload as an anonymous user so the
topic-wide broadcast only carries data any viewer may see, matching
`PollsUpdater.publish_changes`.
- Don't render an empty results panel to a non-staff voter on a
`staff_only` poll; show the ballot and the staff-only notice instead.
Relates to /t/-/185090
And Patch 998
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously, when `Scriptable::Utils.send_pm` raised an unexpected error,
`Jobs::DiscourseAutomation::Tracker#send_pending_pm` only logged it and
left the `PendingPm` row intact, so the scheduled job re-processed the
same failing PM every minute indefinitely.
This change destroys the pending PM in the generic `rescue` block —
mirroring the existing `ActiveRecord::RecordNotSaved` handling — so a
permanently-failing PM is removed after one attempt instead of looping
forever.
Resolves https://patch.discourse.org/patch-triage/14🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Before, staff could create a whisper with a local date in a calendar
topic, which created a visible calendar event from a staff-only post.
This change rejects whispered calendar events, so whispers no longer
create visible calendar entries.
Relates to /patch-triage/993
Before we had a bug because we were using the .then pattern: the
transition continued before the redirect fired. Leading to a brief
flash.
Now beforeModels are using the `async/await` pattern. Specs were added
to cover these changes
## Summary
his PR fixes a bug where Discourse's RSS feeds for
latest/top/hot/category/user-topics/top-by-period advertised a bare
<link> and <atom:link> (e.g. `/latest.rss`) even when the request
included filters like `exclude_tag=...` or `period=.... `Subscribers
were effectively pointed at a
different feed than the one they requested.
## Source
- Patch Triage:patch/891
- Original commit:
---
🤖 Auto-generated from the patch diff via Patch Triage. Review carefully
before merging.
Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
---
Low-risk and no API change.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
This patch fixes a regression where event descriptions lost their
multiline formatting by ensuring newline characters are converted to
<br> tags in the server-generated description_html. It updates the
linkify_description utility to handle both URL linkification and line
break preservation, ensuring the UI correctly renders multiline text.
## Source
- Patch Triage: https://patch.discourse.org/patch-triage/886
- Original commit:
---
🤖 Auto-generated from the patch diff via Patch Triage. Review carefully
before merging.
Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
Inside `type objects` we accepted upload URLs and converted them into
IDs, but we did not update the original object to have an ID.
Only on consuming should we turn the ID into a URL.
For normal front-end consumers, object upload fields should still be
URLs. Backend consumers should expect upload IDs.
<img width="2295" height="1263" alt="Screenshot 2026-05-20 at 12 19 23"
src="https://github.com/user-attachments/assets/20d411c4-1570-45fc-96c8-02f2b1b3639d"
/>
2026-05-21 13:45:27 -03:00
Gabriel Grubbaanddiscourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
Previously, `/chat/new-message` only accepted a `recipients` param to
open a new direct message, with no way to send a message to an existing
channel or pre-fill the composer.
This change adds `channel_id`, `channel` (slug), and `message` params so
external links can drop a user into the right conversation with the
composer ready to send. Currently supported combinations:
- `/chat/new-message?channel_id=42&message=hi`
- `/chat/new-message?channel=general&message=hi`
- `/chat/new-message?recipients=alice,bob`
- `/chat/new-message?recipients=alice&message=hi`
If both `channel_id`/`channel` and `recipients` are passed, the channel
param wins and `recipients` is ignored.
---------
Co-authored-by: discourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
With this change, a user who is ignoring another no longer sees that
user's likes or reactions: the ignored user is hidden from the
likes/reactions lists, and the like and reaction counts shown to the
ignoring user are adjusted accordingly.
Anonymous viewers and viewers who aren't ignoring anyone see the
original counts.
See it in action in the video:
https://github.com/user-attachments/assets/535e9aaa-0a1c-49eb-a2d4-5fdebf9d7539
On https://github.com/discourse/discourse/pull/37694, we ended up
removing one of the nice things of the bootstrap mode, the auto
moderation for the first admin.
I've brought back the job that used to run for the bootstrap mode –
maybe we could just run this code in the `default_current_user_provider`
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Small logic cleanup: we should be checking the domain before doing the
oneboxing.
Sites relying on inline GitHub PR status without `github.com` in
`allowed_inline_onebox_domains` will need to add it.
Also adds a defensive domain/blocklist check inside
`Oneboxer.inline_data_for` itself, and refactors `lookup` to use early
returns.
Removes the duplicate identity-attribute declarations in
BasicVoterSerializer in favor of the canonical chain, leaving only the
voter-specific `direction` attribute on the subclass.
2026-05-05 17:05:04 -03:00
Gabriel Grubbaanddiscourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
The tool runner guardian should be the same as the current user running
the tests.
Co-authored-by: discourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
This is useful for plugins that want to dynamically change the default
categories for the default sidebar categories(anonymous and regular
users when registering)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, themes/plugins customizing the custom homepage had to fetch
data from inside components using AsyncContent or via fetch, which
produced per-component loading spinners after the outlet rendered.
This adds a custom-homepage-model value transformer so themes and
plugins can provide the model (sync or async) at route-load time. The
page renders completely instead of flashing per-block spinners.
Consumers who still want to fetch data after the homepage is loaded will
not be affected by this; the model returning null should not make the
page loading slower
StartPostingOption can now take a `hideAction` method that will
conditionally hide the action button on the start posting modal. This is
useful for cases where we want to show the option but not allow users to
take the action more than once.
This PR allows you to filter topics by the date of your bookmarks.
For example, to find all topics & posts you have bookmarked in the last
week you can run the following search:
```
bookmarked-after:2026-03-08 bookmarked-before:2023-03-15
```
`"filtering for topics by date column"` shared example was not used
because:
- This filter only works for logged-in users
- Bookmark setup and specific test `includes post bookmarks`
This PR corrects the precedence of the `bypass_bump` parameter in
posts_controller.
If you passed `bypass_bump: false` at the top level, it was being
ignored if it had a nested `post[bypass_bump]: true` that was also
present.
Now it should be correct – request bypass_bump param should have
precedence over post bypass bump
A small logic bug for custom scheme URL: some URLs (like
`myapp://callback`) would display with a trailing colon (e.g.,
`callback:`) because `uri.port` is `nil` for non-HTTP scheme.
Also refactored tests
This is an edge case somewhat related to
https://github.com/discourse/discourse/pull/35452
Moderators could unsuspend and unsilence other staff accounts, even tho
they couldn't suspend or silence them in the first place.
Minor cleanup:
- Turned summarize method in chat-modal-channel-summary component to
async/await from promise then/catch.
- Update summary endpoint to use POST instead of GET, as it is
performing an action that changes state and have side effect.
- Added tests
Somewhat a follow-up from
https://github.com/discourse/discourse/pull/36719, that change prevented
the blue dot notification from appearing
We now ensure that when threading is disabled, both
`unreadThreadsCountSinceLastViewed` and `watchedThreadsUnreadCount`
return 0
Also added unit testing to `chat-channel ` service.
Minor cleanup updating how `authenticated_with_oauth` was being called.
`Rack::Session` object supports indifferent access, all
`authenticated_with_oauth` uses are key-based, and this one was off
Also added tests for `LoginActivator` class.
Before, when adding a `type: object` site setting, if it did not match
the schema, the only message returned was `Invalid object.`
With these changes, it is possible to understand which properties did no
match the schema
Updated the action to be a post as it does make side effects, in our
case, it enqueues a job, and updated the client accordingly – I've
checked if there is any other usage of this endpoint in plugins
Also, improved reply action in the client to allow retries when failed,
ensured duplicate requests are not sent, and added their respective
tests.
Introduces a new "informative" block type for chat messages that can
display category information.
This enables bots to present structured category information and other
informational content in chat with blocks.
This is how it looks:
## Examples:
<img width="227" height="129" alt="Screenshot 2026-02-25 at 14 51 52"
src="https://github.com/user-attachments/assets/40f08255-ae5b-4106-a654-8c709bd588e3"
/>
`{ type: "category", title: "Support", color: "0088CC" }`
---
<img width="291" height="136" alt="Screenshot 2026-02-25 at 14 50 56"
src="https://github.com/user-attachments/assets/64815423-672a-42b8-ac4d-b5ccc816b941"
/>
`{ type: "category", title: "Support", color: "0088CC", description:
"Get help from the community" }`
---
<img width="338" height="154" alt="Screenshot 2026-02-25 at 15 02 07"
src="https://github.com/user-attachments/assets/89f820a5-a4d5-41fd-b3e7-8ea674c19a67"
/>
`{ type: "category", title: "Dev", color: "E45735", description:
"Development related discussions", parent_name: "Meta", parent_color:
"0088CC" }`
---
You can have multiple elements per block and they _shoud_ take only one
row:
<img width="459" height="345" alt="Screenshot 2026-02-25 at 15 32 39"
src="https://github.com/user-attachments/assets/710faf9a-c85c-42b2-ab33-5c7e68ea6496"
/>
<img width="530" height="183" alt="Screenshot 2026-02-25 at 15 33 08"
src="https://github.com/user-attachments/assets/d10d444d-b252-42bc-affc-56cd9f5724ca"
/>
## How to use
The only required params are: `type`,`title` and `color`
```rb
ChatSDK::Message.create(
raw: " ",
blocks: [
{
type: "informative",
elements: [
{
type: "category",
title: "Support",
color: "0088CC",
description: "Get help from the community",
},
],
},
],
guardian: bot_guardian,
channel_id: bot_channel_id,
)
```
We have been revamping our admin onboarding, and one of the items on our
list was to remove the bootstrap mode.
This mode was disabled by default and hidden.
We planned to add a new banner for newly created communities to help
guide site owners through initial setup steps.
At the moment, it has 3 items in the checklist:
- Invite collaborators -> Opens invite modal
- Start posting -> Opens composer with a seed icebreaker topic
- Spread the word -> Copies forum link to clipboard
You can see it working in this video:
https://github.com/user-attachments/assets/ed693321-e416-43f9-8182-0243af193688
Current visuals:
<img width="1723" height="393" alt="image"
src="https://github.com/user-attachments/assets/3021c0b1-30e8-4ba6-aa43-aebf71061a8a"
/>
TODO:
- [x] Tests
- [x] Work on CSS
Maybe todos:
- [x] Exclude routes where the banner shouldn't appear
- [x] Add metadata in ice breaker post to ✅ identify it
as an ice breaker topic?
- [x] Check if there is a way to determine if the user is the _first_
admin user?
- [x] Should we have a happy indicator when the user finishes all
tasks(maybe a toast?)
---------
Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
Nameless attachments should be removed too,
now `` will be removed from deleted posts, when
using `Remove Upload Markup From Deleted Posts` automation.
This PR adds the `:tags_for_saving` modifier hook to allow plugins to
modify the list of tags before they are saved to a topic. This is useful
for scenarios where additional tags need to be added or existing tags
need to be altered based on custom logic.
example:
```ruby
register_modifier(:tags_for_saving) do |saving_tags, original_tags, guardian, opts|
# Custom logic to modify saving_tags
saving_tags << "additional-tag" unless saving_tags.include?("additional-tag")
saving_tags
end
```
With this change, S3's `stale-while-revalidate` and `max-age` response
directives can be controlled.
By default, off for `stale-while-revalidate`, and both are hidden site
settings.
Relates to https://github.com/discourse/discourse/pull/36020, we did not
account for posts whose topics have been deleted. In such cases, we need
to provide the Topic context to PostRevisor to avoid errors.
This PR introduces a new automation script that removes uploads attached
to deleted posts.
Using this automation will add a revision to deleted posts by the System
User, removing the upload or attachment references. This Automation goes
a 1000 by every run and does not re-run on already ran posts
When the automation cleans the posts, uploads will no longer have
reference to the post and on the next time the [clean up uploads
job](https://github.com/discourse/discourse/blob/f413e1b0de8c326a12da29f7a4bfa3a6eed004d1/app/jobs/scheduled/clean_up_uploads.rb#L1-L79)
runs it will permanently removes the uploads.
Example of automation run:
```
Hey it is a regular post with a link to [Discourse](https://www.discourse.org)
and an image: 
and a file: [small.pdf|attachment](upload://3bWzVVoRhUXxi7tiPenInoebHyX.pdf) (130 Bytes)
```
When the automation has run in this post, this will be the revisioned
version:
```
Hey it is a regular post with a link to [Discourse](https://www.discourse.org)
and an image:
and a file:
```
---------
Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
The assigned filter should take into account the visibility of the
group.
This PR fixes that and also adds testing to the `assigned:` filter
relates to: https://github.com/discourse/discourse/pull/35712