Skip to content

Feature/discord bot moderation#215

Open
YuriSouzaDev wants to merge 25 commits intohe4rt:4.xfrom
YuriSouzaDev:feature/discord-bot-moderation
Open

Feature/discord bot moderation#215
YuriSouzaDev wants to merge 25 commits intohe4rt:4.xfrom
YuriSouzaDev:feature/discord-bot-moderation

Conversation

@YuriSouzaDev
Copy link
Copy Markdown
Contributor

@YuriSouzaDev YuriSouzaDev commented May 7, 2026

feat(bot-discord): sistema de moderação automática integrado ao Discord

Contexto

O sistema de moderação já existia com uma arquitetura de platform adapters, mas apenas o WebModerationAdapter estava implementado. O Discord é a principal plataforma da comunidade e precisava de um adapter dedicado que executasse ações reais via API e notificasse os usuários por DM.

Referências

O que foi implementado

Pipeline de moderação automática

Toda mensagem recebida no Discord agora passa por uma pipeline completa de análise e ação automática no MessageReceivedEvent:

MessageReceivedEvent
  → IngestContent      (cria o ModerationCase)
  → ClassifyContent    (classifica por regras ou IA)
  → RouteDecision      (define prioridade e ação sugerida)
  → ExecuteAction      (executa nos adapters registrados)

DiscordModerationAdapter

Novo adapter que implementa ModerationPlatformContract e executa ações reais via Discord REST API:

Ação Endpoint Discord Comportamento
Warn Apenas DM embed ao usuário
Mute / Suspend PATCH /guilds/{id}/members/{user} Timeout com communication_disabled_until
Kick DELETE /guilds/{id}/members/{user} Remove o membro do servidor
Ban PUT /guilds/{id}/bans/{user} Ban com remoção de mensagens recentes

Para todas as ações:

  • Envia DM embed ao usuário com tipo da ação, motivo, duração e a mensagem original removida
  • Deleta a mensagem original do canal automaticamente
  • Falhas da API (403, 429, etc.) retornam ExecutionResultDTO::failure() sem crashar o fluxo

Melhorias na pipeline de classificação

  • ClassifyContent: regras manuais têm prioridade sobre a IA — a OpenAI só é chamada quando nenhuma regra bate na mensagem
  • IngestContent: resolve o autor por external_identity quando não vem diretamente no payload do evento
  • RouteDecision: preserva a ação sugerida por regra manual ao atualizar a prioridade do caso

Traduções

Strings do embed de DM adicionadas no namespace moderation::notifications.discord_dm.* para en e pt_BR.

Seeder para desenvolvimento local

PersonalDiscordServerSeeder cria automaticamente o tenant, usuário owner e external_identity vinculada ao HE4RT_DISCORD_GUILD configurado no .env, facilitando o setup do ambiente local com o bot.

Arquivos alterados

Arquivo Tipo Descrição
app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php Novo Adapter principal
app-modules/bot-discord/tests/Feature/Moderation/DiscordModerationAdapterTest.php Novo 20 testes de feature
app-modules/bot-discord/src/Events/MessageReceivedEvent.php Alterado Integração com a pipeline
app-modules/bot-discord/src/BotDiscordServiceProvider.php Alterado Registro do adapter
app-modules/moderation/src/ModerationServiceProvider.php Alterado Remove registro duplicado
app-modules/moderation/src/Classification/Jobs/ClassifyContent.php Alterado Prioridade de regras manuais
app-modules/moderation/src/Classification/Jobs/IngestContent.php Alterado Resolução do autor
app-modules/moderation/src/Classification/Jobs/RouteDecision.php Alterado Preserva ação sugerida
app-modules/moderation/src/DTOs/ModerationContentDTO.php Alterado JsonSerializable + aceita autor
app-modules/moderation/lang/en/notifications.php Alterado Traduções DM embed
app-modules/moderation/lang/pt_BR/notifications.php Alterado Traduções DM embed
database/seeders/PersonalDiscordServerSeeder.php Novo Seeder para dev local
config/discord.php Alterado Chave discord.token
config/services.php Alterado Chave openai.api_key

Cobertura de testes

Os 20 testes do DiscordModerationAdapterTest cobrem:

  • Mute com duração 24h, 7d e 28d (limite máximo do Discord)
  • Kick, Ban (24h, 7d, permanent)
  • Warn e ContentRemove — DM enviada, sem chamada de guild API
  • notify() — abre canal DM e envia mensagem
  • resolveUser() — resolve usuário por Discord ID via external_identities
  • ingest() — transforma payload de evento em ModerationContentDTO
  • Duração desconhecida (999d) — timeout nulo, sem crash
  • Falhas de API: 403 Forbidden, 429 Rate Limit
  • Usuário sem identidade Discord, guild_id e token não configurados

Variáveis de ambiente necessárias

HE4RT_DISCORD_BOT_KEY=   # token do bot Discord (Authorization: Bot {token})
OPENAI_API_KEY=           # chave da OpenAI para classificação por IA
HE4RT_DISCORD_GUILD=      # guild ID para o seeder de desenvolvimento
HE4RT_PRESENTATIONS_CHANNEL_ID=      # id do canal de apresentação
HE4RT_PRESENTATION_ROLE_ID=     # id do cargo depois que o usuario se apresenta

…y — the official postgres:18 image stores data under /var/lib/postgresql/18/main and requires the volume to

   be mounted at the parent path /var/lib/postgresql.
…na pipeline de classificação

- ClassifyContent: regras manuais têm precedência sobre a IA; OpenAI só é chamada quando nenhuma regra bate
- IngestContent: resolve autor por external_identity quando não vem diretamente no payload
- RouteDecision: preserva a ação sugerida por regra ao atualizar prioridade do caso
…k, ban, warn e DM de notificação

- Executa ações de moderação via Discord REST API (PATCH timeout, DELETE kick, PUT ban)
- Envia DM embed ao usuário com tipo, motivo, duração e mensagem original removida
- Remove a mensagem original do canal após a ação
- Registra e tageia o adapter no BotDiscordServiceProvider
- Remove registro duplicado do adapter que estava no ModerationServiceProvider
- Remove método label() vazio que havia sido adicionado por engano ao ActionType
…o de mensagem recebida

- Ingere cada mensagem recebida como ModerationContentDTO
- Executa ClassifyContent e RouteDecision em sequência (dispatch_sync)
- Cria ModerationAction e dispara ExecuteAction quando há ação sugerida
- Resolve identidade do autor por external_identity quando não vem no evento
…apter

Cobre mute (24h, 7d, 28d), kick, ban (24h, 7d, permanente), warn, content_remove,
notify, resolveUser, ingest, duração inválida e falhas de API (403, 429,
sem identidade Discord, guild_id e token não configurados)
…envolvimento local

Cria tenant, usuário owner e external_identity vinculada ao guild_id configurado
no .env para facilitar o setup do ambiente de desenvolvimento com o bot do Discord
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

Warning

Rate limit exceeded

@YuriSouzaDev has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 38 minutes and 44 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro

Run ID: d6a549c8-61f9-473f-99a8-a06f77dbb072

📥 Commits

Reviewing files that changed from the base of the PR and between 82f9558 and 2285bd5.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • PR.md
  • app-modules/moderation/src/Classification/Actions/Advisors/HistoryBasedPenaltyAdvisor.php
  • app-modules/moderation/src/DTOs/ModerationContentDTO.php
  • app-modules/moderation/tests/Feature/Enforcement/ExecuteActionTest.php
📝 Walkthrough

Walkthrough

This PR integrates Discord message moderation by introducing a DiscordModerationAdapter that implements the moderation platform contract. Discord messages are now routed through an automated pipeline: the adapter ingests message payloads into ModerationContentDTO, the pipeline runs rule-based and AI classification to detect violations and compute suggested actions, routing logic applies penalty escalation rules, and automated ModerationAction records are persisted and executed synchronously. Execution dispatches Discord API calls for mute/ban/kick (with configurable durations capped at 28 days), sends localized DM notifications to moderated users, and deletes original messages. Supporting changes update configuration sourcing, add comprehensive test coverage including error scenarios, and include database seeding for Discord server setup.

Possibly related PRs

  • he4rt/heartdevs.com#187: Modifies MessageReceivedEvent.php morph-type lookup logic alongside this PR's handler signature and pipeline integration changes.
  • he4rt/heartdevs.com#186: Restructures ExternalIdentity schema with provider_idexternal_account_id renaming, directly impacting this PR's Discord identity resolution and seeder creation logic.

Suggested reviewers

  • gvieira18
  • Clintonrocha98
  • thalesmengue
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/discord bot moderation' directly corresponds to the main objective of integrating an automatic moderation system for Discord messages. It clearly identifies the feature being added.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app-modules/bot-discord/src/Events/MessageReceivedEvent.php (1)

47-73: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The pre-resolved author identity is dropped before reaching IngestContent.

'author' => $authorIdentity?->user is passed into DiscordModerationAdapter::ingest(), but the adapter's ingest() (see DiscordModerationAdapter.php lines 35-50) only forwards a fixed shape (content_id, content_type, author_external_id, text, media_urls, tenant_id, metadata) to ModerationContentDTO::fromPlatform. The author key is never propagated to the DTO, so IngestContent::handle() ends up running the same ExternalIdentity query again. Either:

  • propagate author through ingest() into the DTO so the lookup is short-circuited, or
  • drop the redundant $authorIdentity lookup here and let IngestContent handle resolution.

Either way the duplicate query should go. Also consider adding a model_type filter to this query (and the one in IngestContent) to avoid matching identities not bound to a User.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/bot-discord/src/Events/MessageReceivedEvent.php` around lines 47
- 73, The pre-resolved ExternalIdentity retrieved in MessageReceivedEvent is
never propagated into the moderation DTO, causing a duplicate ExternalIdentity
query; either pass the resolved user through DiscordModerationAdapter::ingest()
into ModerationContentDTO::fromPlatform so the DTO (and subsequently
IngestContent::handle) can use the provided author (short-circuiting the
lookup), or remove the ExternalIdentity lookup here and let IngestContent
perform resolution; in both places that query (the one in MessageReceivedEvent
and the one in IngestContent) add a model_type (or model_id) filter to the
ExternalIdentity::query() to ensure you only match identities bound to a User so
you don't return unrelated identities.
🧹 Nitpick comments (2)
app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php (1)

102-124: ⚡ Quick win

Extract the duplicated DM-channel creation logic.

notify() (lines 111-123) and sendDmNotification() (lines 211-236) both POST /users/@me/channels with a recipient_id, check failed(), then POST to /channels/{id}/messages. The opening of the DM channel is identical and could be extracted into a single helper (e.g., openDmChannel(string $token, string $discordId): ?string) returning the channel id or null. This also lets you guard against an empty id in the response ((string) $dmResponse->json('id') currently coerces null to '', which would then post to /channels//messages).

♻️ Sketch
+    private function openDmChannel(string $token, string $discordId): ?string
+    {
+        $response = Http::withHeaders(['Authorization' => 'Bot '.$token])
+            ->timeout(5)
+            ->post('https://discord.com/api/v10/users/@me/channels', [
+                'recipient_id' => $discordId,
+            ]);
+
+        if ($response->failed()) {
+            return null;
+        }
+
+        $id = $response->json('id');
+
+        return is_string($id) && $id !== '' ? $id : null;
+    }

Then notify() and sendDmNotification() can both call it and bail early on null.

Also applies to: 209-237

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php` around
lines 102 - 124, The DM-channel creation and message sending is duplicated in
notify() and sendDmNotification(); extract the POST /users/@me/channels logic
into a helper like openDmChannel(string $token, string $discordId): ?string that
performs the channel-creation request, checks ->failed(), reads and validates
the response id (return null if missing/empty), and returns the channel id; then
update notify() and sendDmNotification() to call openDmChannel() and bail early
on null before posting to /channels/{id}/messages so you never post to
/channels//messages.
database/seeders/PersonalDiscordServerSeeder.php (1)

35-49: ⚡ Quick win

Make user/tenant creation idempotent.

The early-return check only verifies the ExternalIdentity row. If a previous seeder run created the User (or Tenant) but failed before inserting the identity, re-running will hit unique-constraint violations on users.email/users.username. Prefer firstOrCreate for the owner user (and similarly for the tenant) so the seeder is fully re-runnable.

♻️ Proposed fix
-        $owner = User::factory()->create([
-            'username' => 'local-owner',
-            'name' => 'Local Owner',
-            'email' => 'local@local.dev',
-            'password' => Hash::make('password'),
-        ]);
+        $owner = User::query()->firstOrCreate(
+            ['email' => 'local@local.dev'],
+            [
+                'username' => 'local-owner',
+                'name' => 'Local Owner',
+                'password' => Hash::make('password'),
+            ],
+        );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@database/seeders/PersonalDiscordServerSeeder.php` around lines 35 - 49, The
seeder should avoid unique-constraint failures by making owner and tenant
creation idempotent: replace the direct User::factory()->create call used to set
$owner with a lookup/create (e.g., User::firstOrCreate using the unique keys
'email'/'username' and setting name/password if created), and likewise replace
the Tenant::factory()->...->create that sets $tenant with a firstOrCreate or
firstOrNew + save keyed by 'slug' (ensuring owner relationship is set via
for/owner or by assigning owner_id), then ensure members() attach is executed
only when the tenant was newly created or when the owner is not already
attached; update any afterCreating logic (Tenant::factory()->afterCreating) to
be conditional or move the attach into an if (!
$tenant->members()->where('id',$owner->id)->exists()) {
$tenant->members()->attach($owner); } so re-running the seeder is safe.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app-modules/bot-discord/src/Events/MessageReceivedEvent.php`:
- Around line 84-97: The current synchronous dispatch in
MessageReceivedEvent::handle (dispatch_sync(new ExecuteAction(...))) blocks the
Laracord websocket loop; replace the synchronous call with an
asynchronous/queued dispatch (e.g., dispatch(new ExecuteAction(...)) or
ExecuteAction::dispatch(...)) so ExecuteAction (which implements ShouldQueue)
runs off the websocket loop, and also add explicit HTTP timeouts in
DiscordModerationAdapter::execute for all Http::* calls (use ->timeout(...) or
equivalent) to prevent long hangs if the remote Discord API is slow.

In `@app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php`:
- Around line 111-124: Add explicit HTTP timeouts to all Discord API outbound
calls to avoid blocking on slow/unresponsive requests; update every
Http::withHeaders(...)->post|patch|put|delete(...) invocation in the
DiscordModerationAdapter (notably inside banMember, kickMember,
deleteOriginalMessage, sendDmNotification, and notify) to include a sensible
timeout and connectTimeout (e.g., ->timeout(15)->connectTimeout(5)) so that each
request fails fast instead of relying on system Guzzle/cURL defaults.

In `@app-modules/moderation/src/Classification/Jobs/IngestContent.php`:
- Around line 32-38: The fallback ExternalIdentity lookup (the block using
ExternalIdentity::query() to resolve $authorId by Discord) needs to be scoped by
tenant: add a where('tenant_id', $this->content->tenantId) to the query so it
matches the tenant of the current content; update the query that chains
where('model_type', (new User)->getMorphClass()), where('provider',
IdentityProvider::Discord), where('external_account_id',
$this->content->authorExternalId') to also include where('tenant_id',
$this->content->tenantId') so the resolved model_id belongs to the same tenant.

In `@app-modules/moderation/src/DTOs/ModerationContentDTO.php`:
- Around line 67-76: jsonSerialize() currently emits 'text_content' while
fromPlatform() expects 'text', causing the DTO round-trip to drop text; update
one side to use the same key—best fix: change jsonSerialize() to emit 'text'
(replace the 'text_content' array key with 'text') so it matches fromPlatform(),
and ensure the ModerationContentDTO::jsonSerialize() and
ModerationContentDTO::fromPlatform() keys are identical.
- Line 58: The author field is assigned directly from $rawPayload['author'] to a
?User property which can cause a TypeError if an array or other type is passed;
update the assignment in ModerationContentDTO (the construction/mapping logic
that sets author from $rawPayload) to validate the type using an instanceof User
guard (e.g., set author to $rawPayload['author'] if it is a User, otherwise
null) so only a User or null is assigned.

In `@app-modules/moderation/tests/Feature/Classification/RouteDecisionTest.php`:
- Around line 167-179: The test 'keeps discord high severity cases open when ai
scores are empty' conflicts with current RouteDecision behavior: when ai_scores
is empty RouteDecision computes maxScore=0 and will dismiss the case; update the
test expectation to assert the case becomes dismissed instead of remaining
pending. Locate the test function (the RouteDecisionTest case using
ModerationCase::factory with ai_scores => [] and source_platform =>
Platform::Discord) and change the final expectation from CaseStatus::Pending to
the dismissal status matching your enum/constant used by RouteDecision (e.g.,
CaseStatus::Dismissed).

In `@bootstrap/providers.php`:
- Line 8: The import and registration of HubPanelProvider will cause a fatal
error because the class is missing; create a new class named HubPanelProvider in
the App\Providers\Filament namespace that declares class HubPanelProvider
extends Illuminate\Support\ServiceProvider (or the correct Filament provider
base if used), implement at minimum empty public register() and public boot()
methods, and ensure its FQCN matches the use statement so Laravel can
instantiate it; alternatively, if the provider is not needed, remove the use
HubPanelProvider; and ensure the providers array still references the correct
class name used throughout the project.

---

Outside diff comments:
In `@app-modules/bot-discord/src/Events/MessageReceivedEvent.php`:
- Around line 47-73: The pre-resolved ExternalIdentity retrieved in
MessageReceivedEvent is never propagated into the moderation DTO, causing a
duplicate ExternalIdentity query; either pass the resolved user through
DiscordModerationAdapter::ingest() into ModerationContentDTO::fromPlatform so
the DTO (and subsequently IngestContent::handle) can use the provided author
(short-circuiting the lookup), or remove the ExternalIdentity lookup here and
let IngestContent perform resolution; in both places that query (the one in
MessageReceivedEvent and the one in IngestContent) add a model_type (or
model_id) filter to the ExternalIdentity::query() to ensure you only match
identities bound to a User so you don't return unrelated identities.

---

Nitpick comments:
In `@app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php`:
- Around line 102-124: The DM-channel creation and message sending is duplicated
in notify() and sendDmNotification(); extract the POST /users/@me/channels logic
into a helper like openDmChannel(string $token, string $discordId): ?string that
performs the channel-creation request, checks ->failed(), reads and validates
the response id (return null if missing/empty), and returns the channel id; then
update notify() and sendDmNotification() to call openDmChannel() and bail early
on null before posting to /channels/{id}/messages so you never post to
/channels//messages.

In `@database/seeders/PersonalDiscordServerSeeder.php`:
- Around line 35-49: The seeder should avoid unique-constraint failures by
making owner and tenant creation idempotent: replace the direct
User::factory()->create call used to set $owner with a lookup/create (e.g.,
User::firstOrCreate using the unique keys 'email'/'username' and setting
name/password if created), and likewise replace the
Tenant::factory()->...->create that sets $tenant with a firstOrCreate or
firstOrNew + save keyed by 'slug' (ensuring owner relationship is set via
for/owner or by assigning owner_id), then ensure members() attach is executed
only when the tenant was newly created or when the owner is not already
attached; update any afterCreating logic (Tenant::factory()->afterCreating) to
be conditional or move the attach into an if (!
$tenant->members()->where('id',$owner->id)->exists()) {
$tenant->members()->attach($owner); } so re-running the seeder is safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 004e1e7c-abdf-430b-b8b2-9659b50b253e

📥 Commits

Reviewing files that changed from the base of the PR and between 28212f0 and 9c1a942.

⛔ Files ignored due to path filters (2)
  • composer.lock is excluded by !**/*.lock
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • app-modules/bot-discord/src/BotDiscordServiceProvider.php
  • app-modules/bot-discord/src/Events/MessageReceivedEvent.php
  • app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php
  • app-modules/bot-discord/tests/Feature/Moderation/DiscordModerationAdapterTest.php
  • app-modules/moderation/lang/en/notifications.php
  • app-modules/moderation/lang/pt_BR/notifications.php
  • app-modules/moderation/src/Classification/Jobs/ClassifyContent.php
  • app-modules/moderation/src/Classification/Jobs/IngestContent.php
  • app-modules/moderation/src/Classification/Jobs/RouteDecision.php
  • app-modules/moderation/src/DTOs/ModerationContentDTO.php
  • app-modules/moderation/tests/Feature/Classification/RouteDecisionTest.php
  • app-modules/moderation/tests/Unit/PenaltyAdvisorTest.php
  • bootstrap/providers.php
  • composer.json
  • config/discord.php
  • config/services.php
  • database/seeders/PersonalDiscordServerSeeder.php

Comment thread app-modules/bot-discord/src/Events/MessageReceivedEvent.php
Comment thread app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php Outdated
Comment thread app-modules/moderation/src/Classification/Jobs/IngestContent.php
Comment thread app-modules/moderation/src/DTOs/ModerationContentDTO.php Outdated
Comment thread app-modules/moderation/src/DTOs/ModerationContentDTO.php
Comment thread bootstrap/providers.php Outdated
…bloquear o loop do websocket

dispatch_sync bloqueava o handler de mensagens durante todas as chamadas HTTP ao Discord.
Como ExecuteAction já implementa ShouldQueue, dispatch() delega ao worker sem travar o loop.
…0s no DiscordModerationAdapter

Todas as 8 chamadas à Discord API passam agora pelo helper http() que aplica
Authorization e timeout(10) em um único lugar, evitando jobs presos por resposta lenta da API.
…estContent

(new User)->getMorphClass() instanciava um model para obter apenas uma string de classe.
Substituído por User::class, que é equivalente em apps sem morph map configurado.
… do ModerationContentDTO

$rawPayload['author'] pode ser array em payloads JSON deserializados. Sem a guarda,
um array causaria TypeError na propriedade ?User em PHP 8.1+.
@YuriSouzaDev
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php`:
- Around line 112-124: Ensure we validate the DM channel id returned from the
create-DM call before attempting to post a message: in
DiscordModerationAdapter.php where $dmResponse = $this->http($token)->post(...
users/@me/channels ...) and later used as $dmResponse->json('id'), check that
$dmResponse->successful() and that $dmResponse->json('id') is a non-empty
string/number; if it is missing, log or handle the error and return early
instead of calling
post(sprintf('https://discord.com/api/v10/channels/%s/messages', ...)). Apply
the same guard to the other flow that reads $dmResponse->json('id') (the block
around lines 226–230) so you never call /channels//messages when the create-DM
response is malformed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 73f770fa-1d6a-4fb3-bd55-a4d98a52abcf

📥 Commits

Reviewing files that changed from the base of the PR and between 9c1a942 and a8cda07.

📒 Files selected for processing (5)
  • app-modules/bot-discord/src/Events/MessageReceivedEvent.php
  • app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php
  • app-modules/moderation/src/Classification/Jobs/IngestContent.php
  • app-modules/moderation/src/DTOs/ModerationContentDTO.php
  • app-modules/moderation/tests/Feature/Classification/RouteDecisionTest.php
✅ Files skipped from review due to trivial changes (1)
  • app-modules/moderation/src/Classification/Jobs/IngestContent.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • app-modules/moderation/tests/Feature/Classification/RouteDecisionTest.php
  • app-modules/bot-discord/src/Events/MessageReceivedEvent.php

Comment thread app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php`:
- Around line 76-89: The execute() flow currently calls sendDmNotification() and
deleteOriginalMessage() before validating the main enforcement response, which
can produce misleading side-effects; change execute() (the method handling
$response) to first verify that $response is a Response and not
$response->failed() and only then invoke sendDmNotification() and
deleteOriginalMessage(); refactor those helpers (sendDmNotification and
deleteOriginalMessage) to return Response|null (or a result object) instead of
hiding HTTP results so callers can log/handle failures, and update execute() to
inspect those returned values and propagate failure via ExecutionResultDTO when
any side-effect response indicates failure; apply the same pattern to the other
affected blocks (the other execute()/enforcement branches referenced).
- Around line 167-174: parseDuration() can return null and timeoutMember()
currently proceeds to call Discord anyway; add an explicit guard in
timeoutMember(string $token, string $guildId, string $discordId, ?string
$duration) to treat a null $until as an invalid duration and bail out early
(return a 4xx/invalid input response or throw a clear exception) instead of
calling $this->http(...)->patch(...); do the same for the other method that uses
parseDuration and sets 'communication_disabled_until' (the same parseDuration
usage around lines 197-203) so no PATCH is issued when the duration is invalid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 343be517-9574-4306-8a13-26c0932130c5

📥 Commits

Reviewing files that changed from the base of the PR and between a8cda07 and 82f9558.

📒 Files selected for processing (1)
  • app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php

Comment on lines +76 to +89
$this->sendDmNotification($token, $discordId, $action);

try {
$this->deleteOriginalMessage($token, $action);
} catch (Throwable) {
// message may have already been deleted or case snapshot is missing
}

if (!$response instanceof Response) {
return ExecutionResultDTO::success(Platform::Discord, ['action' => $action->action_type->value]);
}

if ($response->failed()) {
return ExecutionResultDTO::failure(Platform::Discord, $response->body());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Validate the enforcement call before firing notification/deletion side effects.

execute() sends the DM and attempts message deletion before it checks $response->failed(), and both helpers hide their own HTTP result from the caller. A rejected kick/ban/mute can therefore still notify the user as if the action happened, and partial Discord-side failures stay invisible to the pipeline. Check the main moderation response first, then have the side-effect helpers return Response|null so their outcome can be accounted for too.

Also applies to: 207-218, 220-252

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php` around
lines 76 - 89, The execute() flow currently calls sendDmNotification() and
deleteOriginalMessage() before validating the main enforcement response, which
can produce misleading side-effects; change execute() (the method handling
$response) to first verify that $response is a Response and not
$response->failed() and only then invoke sendDmNotification() and
deleteOriginalMessage(); refactor those helpers (sendDmNotification and
deleteOriginalMessage) to return Response|null (or a result object) instead of
hiding HTTP results so callers can log/handle failures, and update execute() to
inspect those returned values and propagate failure via ExecutionResultDTO when
any side-effect response indicates failure; apply the same pattern to the other
affected blocks (the other execute()/enforcement branches referenced).

Comment on lines +167 to +174
private function timeoutMember(string $token, string $guildId, string $discordId, ?string $duration): Response
{
$until = $this->parseDuration($duration);

return $this->http($token)
->patch(sprintf('https://discord.com/api/v10/guilds/%s/members/%s', $guildId, $discordId), [
'communication_disabled_until' => $until?->format(DateTimeInterface::ATOM),
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject unsupported timeout durations before calling Discord.

parseDuration() falls back to null, and timeoutMember() still issues the PATCH. That makes a typo/new duration value a silent success path for Mute/Suspend instead of a hard failure. Treat null as invalid here and bail out early.

Suggested guard
 private function timeoutMember(string $token, string $guildId, string $discordId, ?string $duration): Response
 {
     $until = $this->parseDuration($duration);
+    if ($until === null) {
+        throw new \InvalidArgumentException('Unsupported Discord timeout duration.');
+    }
 
     return $this->http($token)
         ->patch(sprintf('https://discord.com/api/v10/guilds/%s/members/%s', $guildId, $discordId), [
             'communication_disabled_until' => $until?->format(DateTimeInterface::ATOM),
         ]);

Also applies to: 197-203

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php` around
lines 167 - 174, parseDuration() can return null and timeoutMember() currently
proceeds to call Discord anyway; add an explicit guard in timeoutMember(string
$token, string $guildId, string $discordId, ?string $duration) to treat a null
$until as an invalid duration and bail out early (return a 4xx/invalid input
response or throw a clear exception) instead of calling
$this->http(...)->patch(...); do the same for the other method that uses
parseDuration and sets 'communication_disabled_until' (the same parseDuration
usage around lines 197-203) so no PATCH is issued when the duration is invalid.

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.

3 participants