From 44e7760c93f4332474844445e6134608482128d9 Mon Sep 17 00:00:00 2001 From: sulimanbenhalim Date: Thu, 22 Oct 2026 02:33:23 +0200 Subject: [PATCH] fix: stop people in timeout from opening issues --- KNOWLEDGE_BASE.md | 2 +- resources/js/components/issue-command.tsx | 17 ++++++++++++----- routes/web.php | 2 +- .../Controllers/UserTimeoutControllerTest.php | 10 ++++++++++ 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/KNOWLEDGE_BASE.md b/KNOWLEDGE_BASE.md index 90543581e..382ccce4e 100644 --- a/KNOWLEDGE_BASE.md +++ b/KNOWLEDGE_BASE.md @@ -484,7 +484,7 @@ Posts awaiting automated checks stay pending if those checks fail; a failed chec Before your very first commit, reply, or quote, the composer shows the commit rules, and keeps showing them on every attempt until you send with "Got it, commit": no engagement bait, no low-effort posts, no AI-generated posts, no spam or self-promotion, and no harassment, hate, or private information, followed by a link to the terms. -Moderators can put someone in **timeout** for 1 hour, 1 day, 3 days, 7 days, or 30 days, with a reason taken from the commit rules. During a timeout the person can still read and react, but cannot commit, reply, quote, schedule, send remotes, or start, rename, or add people to orgs from any surface; each attempt says how long is left and why. Scheduled commits wait until the timeout ends. Moderators can end a timeout early, and a profile shows moderators how many timeouts the person has had, ended ones included. Like bans, timeouts belong to the GitHub account: deleting the /dev/null account and signing in again neither ends a timeout nor resets the count. +Moderators can put someone in **timeout** for 1 hour, 1 day, 3 days, 7 days, or 30 days, with a reason taken from the commit rules. During a timeout the person can still read and react, but cannot commit, reply, quote, schedule, open issues, send remotes, or start, rename, or add people to orgs from any surface; each attempt says how long is left and why. Scheduled commits wait until the timeout ends. Moderators can end a timeout early, and a profile shows moderators how many timeouts the person has had, ended ones included. Like bans, timeouts belong to the GitHub account: deleting the /dev/null account and signing in again neither ends a timeout nor resets the count. Moderators can also **ban** someone, with a reason. A banned person is sent from every page to one that says why, where they can only sign out or delete their account; the app refuses everything but deleting the account, and the terminal refuses them outright. A ban belongs to the GitHub account, so deleting the /dev/null account and signing in again does not lift it. Banning someone cancels their scheduled commits, so lifting the ban later doesn't send them all at once. Admins cannot be put in timeout or banned. Moderation → **Timeouts & bans** lists everyone in timeout and every ban, where moderators can end a timeout or lift a ban, including one on a deleted account. diff --git a/resources/js/components/issue-command.tsx b/resources/js/components/issue-command.tsx index 62740aa59..124440ba1 100644 --- a/resources/js/components/issue-command.tsx +++ b/resources/js/components/issue-command.tsx @@ -4,6 +4,7 @@ import type { KeyboardEvent } from 'react'; import { useState } from 'react'; import { toast } from 'sonner'; import { store } from '@/actions/App/Http/Controllers/IssueController'; +import { showTimeout, timeoutFrom } from '@/components/timeout-dialog'; import { collectEnvironment } from '@/lib/environment'; import type { IssueEnvironment } from '@/lib/environment'; import type { IssueCommand, IssueKind } from '@/lib/issue-commands'; @@ -104,11 +105,17 @@ export function useIssueCommand({ ); }, onHttpException: (response) => { - toast.error( - response.status === 429 - ? 'Issue limit reached: 10 a day. Try again tomorrow.' - : 'GitHub did not confirm the issue. Check upstream before retrying. Your draft is safe.', - ); + const timeout = timeoutFrom(response.status, response.data); + + if (timeout) { + showTimeout(timeout); + } else { + toast.error( + response.status === 429 + ? 'Issue limit reached: 10 a day. Try again tomorrow.' + : 'GitHub did not confirm the issue. Check upstream before retrying. Your draft is safe.', + ); + } }, onNetworkError: () => { toast.error( diff --git a/routes/web.php b/routes/web.php index 5ef875a03..7980f83c5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -232,7 +232,7 @@ Route::middleware('auth')->group(function (): void { Route::post('issues', [IssueController::class, 'store']) - ->middleware('throttle:10,1') + ->middleware([PreventPostingInTimeout::class, 'throttle:10,1']) ->name('issue.store'); // Session... diff --git a/tests/Feature/Controllers/UserTimeoutControllerTest.php b/tests/Feature/Controllers/UserTimeoutControllerTest.php index c8c82fc38..a04f8b561 100644 --- a/tests/Feature/Controllers/UserTimeoutControllerTest.php +++ b/tests/Feature/Controllers/UserTimeoutControllerTest.php @@ -194,6 +194,16 @@ ->and(Conversation::query()->sole()->participants()->count())->toBe(2); }); +it('stops a user in timeout from opening issues', function (): void { + $user = User::factory()->create(); + UserTimeout::factory()->for($user)->create(); + + $this->actingAs($user) + ->postJson(route('issue.store'), ['command' => '/bug', 'body' => 'buy my course']) + ->assertForbidden() + ->assertJsonPath('timeout.reason', 'Spam or self-promotion'); +}); + it('tells the user how long is left of the timeout that ends last, rounded up', function (string $endsIn, string $remaining): void { $user = User::factory()->create(); UserTimeout::factory()->for($user)->create(['ends_at' => now()->addMinutes(30)]);