Skip to content

Feature/discord bot moderation#215

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

Feature/discord bot moderation#215
YuriSouzaDev wants to merge 30 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

Como testar o bot:

  • Adicione as variaveis ao seu .env
  • Rode o comando do novo seeder
  • E no dashboard altere conforme a imagem o tenant
image

…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

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a DiscordModerationAdapter and registers it as a moderation platform, ingests Discord messages into ModerationContentDTO, runs rule-first then AI classification, preserves rule-derived suggested_action, applies revised penalty-escalation logic, and executes automated ModerationAction records (mute/ban/kick via Discord API with DM notifications and optional message deletion). It also updates DTO serialization, adds OpenAI config, expands adapter and pipeline tests, adds discord DM translations, and provides a PersonalDiscordServerSeeder.

Possibly related issues

Possibly related PRs

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.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: integrating an automatic moderation system for Discord into the existing moderation pipeline.
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
…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 Outdated
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 thread app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php Outdated
Comment thread app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php Outdated
…ar o proprietário existente e aprimorar a lógica de busca do autor em IngestContent
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

🧹 Nitpick comments (1)
app-modules/moderation/tests/Feature/Enforcement/ExecuteActionTest.php (1)

95-110: ⚡ Quick win

Expand verification to include Discord execution results.

Now that Discord is a registered moderation platform, the test "handles multiple target platforms" should verify execution results for both 'web' and 'discord' platforms. Currently, only the 'web' result is checked, leaving a test coverage gap.

✅ Proposed enhancement to verify both platforms
     $action->refresh();
+    expect($action->execution_results)->toHaveCount(2);
+    
     $webResult = collect($action->execution_results)->firstWhere('platform', 'web');
     expect($webResult)->not->toBeNull()
         ->and($webResult['success'])->toBeTrue();
+    
+    $discordResult = collect($action->execution_results)->firstWhere('platform', 'discord');
+    expect($discordResult)->not->toBeNull()
+        ->and($discordResult['success'])->toBeTrue();
 });
🤖 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/moderation/tests/Feature/Enforcement/ExecuteActionTest.php`
around lines 95 - 110, The test "handles multiple target platforms" only asserts
the 'web' execution result; update the test in
ExecuteActionTest::testHandlesMultipleTargetPlatforms to also locate the Discord
result from $action->execution_results (e.g. $discordResult =
collect($action->execution_results)->firstWhere('platform', 'discord')) and add
assertions mirroring the web check (not null and success true) so both 'web' and
'discord' execution outcomes are verified after new ExecuteAction($action,
$user)->handle() and $action->refresh().
🤖 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 `@database/seeders/PersonalDiscordServerSeeder.php`:
- Line 34: The seeder calls User::query()->first() into $owner without checking
for null, which will cause exceptions when later used in methods like
->for($owner, 'owner') and ->attach($owner); update PersonalDiscordServerSeeder
to check if $owner is null and handle it (e.g., create a fallback user, skip
seeding with a clear log/notice, or throw a descriptive exception) before
calling ->for(...) or ->attach(...), ensuring any branch uses a valid User
instance.
- Line 52: The seeder is creating empty Discord credentials because
ClientAccessManager::make() is called without parameters; update the
'credentials' value in the PersonalDiscordServerSeeder to pass the encrypted bot
token into ClientAccessManager::make() (use
Crypt::encrypt(config('discord.token')) as the apiKey argument, or pass an
accessToken param if you’re using OAuth2) so the created credentials contain the
token needed for authentication.

---

Nitpick comments:
In `@app-modules/moderation/tests/Feature/Enforcement/ExecuteActionTest.php`:
- Around line 95-110: The test "handles multiple target platforms" only asserts
the 'web' execution result; update the test in
ExecuteActionTest::testHandlesMultipleTargetPlatforms to also locate the Discord
result from $action->execution_results (e.g. $discordResult =
collect($action->execution_results)->firstWhere('platform', 'discord')) and add
assertions mirroring the web check (not null and success true) so both 'web' and
'discord' execution outcomes are verified after new ExecuteAction($action,
$user)->handle() and $action->refresh().
🪄 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: db87fa81-1344-4674-a607-076af0c03d09

📥 Commits

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

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • PR.md
  • app-modules/moderation/src/Classification/Actions/Advisors/HistoryBasedPenaltyAdvisor.php
  • app-modules/moderation/src/Classification/Jobs/IngestContent.php
  • app-modules/moderation/src/DTOs/ModerationContentDTO.php
  • app-modules/moderation/tests/Feature/Enforcement/ExecuteActionTest.php
  • database/seeders/PersonalDiscordServerSeeder.php
✅ Files skipped from review due to trivial changes (1)
  • PR.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • app-modules/moderation/src/Classification/Jobs/IngestContent.php
  • app-modules/moderation/src/DTOs/ModerationContentDTO.php

return;
}

$owner = User::query()->first();
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 | 🔴 Critical | ⚡ Quick win

Add null check for $owner to prevent runtime errors.

If no users exist in the database, User::query()->first() will return null, causing runtime exceptions when passed to ->for($owner, 'owner') and ->attach($owner) in subsequent lines.

🛡️ Proposed fix
 $owner = User::query()->first();
+
+if ($owner === null) {
+    $this->command->error('Nenhum usuário encontrado no banco de dados. Crie um usuário antes de executar este seeder.');
+    return;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$owner = User::query()->first();
$owner = User::query()->first();
if ($owner === null) {
$this->command->error('Nenhum usuário encontrado no banco de dados. Crie um usuário antes de executar este seeder.');
return;
}
🤖 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` at line 34, The seeder
calls User::query()->first() into $owner without checking for null, which will
cause exceptions when later used in methods like ->for($owner, 'owner') and
->attach($owner); update PersonalDiscordServerSeeder to check if $owner is null
and handle it (e.g., create a fallback user, skip seeding with a clear
log/notice, or throw a descriptive exception) before calling ->for(...) or
->attach(...), ensuring any branch uses a valid User instance.

'type' => IdentityType::External,
'provider' => IdentityProvider::Discord,
'credentials_type' => CredentialsType::ApiKey,
'credentials' => ClientAccessManager::make(),
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

🧩 Analysis chain

🌐 Web query:

he4rt/identity package ClientAccessManager usage for Discord API credentials

💡 Result:

I couldn’t find any “he4rt/identity” package or any “ClientAccessManager” symbol in authoritative sources during this research pass. What I can say from the sources I found is: 1) Discord credential usage is typically via OAuth2 flows that require a Discord application’s client_id and client_secret (and Discord explicitly warns to keep these secrets out of source code). Discord’s OAuth2 docs state that OAuth2 calls require either HTTP Basic auth or client_id/client_secret in the request body, and discuss the client credentials grant for bots [1]. 2) The he4rt repositories I found relate to Discord bots (e.g., he4rt/He4rt-Bot and he4rt/he4rt-bot-next), but the search results shown here do not expose any “ClientAccessManager” usage or configuration patterns for Discord credentials [2][3]. If you can share the exact repository/package URL for “he4rt/identity” (or the GitHub/npm/Packagist link) and where you saw “ClientAccessManager” referenced, I can re-run targeted searching on that exact codebase and extract the precise usage pattern for Discord credentials. For now, the only grounded, generally applicable guidance from Discord’s own docs is: - Treat Discord OAuth2 client_id/client_secret as secrets; Discord’s OAuth2 docs state the credentials must be provided for OAuth2 token calls and warns to be careful about where they live [1]. Sources: [1][3][2]

Citations:


🏁 Script executed:

# Find ClientAccessManager class definition
find . -type f -name "*.php" | xargs grep -l "class ClientAccessManager" 2>/dev/null | head -20

Repository: he4rt/heartdevs.com

Length of output: 136


🏁 Script executed:

# Search for ClientAccessManager usage throughout codebase
rg "ClientAccessManager" --type php -A 2 -B 2

Repository: he4rt/heartdevs.com

Length of output: 17676


🏁 Script executed:

# Search for Discord token configuration references
rg "discord.*token|token.*discord" --type php -i -A 2 -B 2

Repository: he4rt/heartdevs.com

Length of output: 11576


🏁 Script executed:

# Check config directory for Discord settings
find . -type f \( -name "*.php" -o -name "*.env*" -o -name "*.json" \) | xargs grep -l "discord" -i 2>/dev/null | head -20

Repository: he4rt/heartdevs.com

Length of output: 1229


🏁 Script executed:

cat -n ./app-modules/identity/src/ExternalIdentity/Data/ClientAccessManager.php | head -100

Repository: he4rt/heartdevs.com

Length of output: 3461


Pass the Discord bot token to ClientAccessManager::make() at line 52.

ClientAccessManager::make() called without parameters creates credentials with all properties set to null. This will result in an empty credentials object that cannot authenticate with Discord. Pass the encrypted Discord token:

'credentials' => ClientAccessManager::make(
    apiKey: Crypt::encrypt(config('discord.token'))
),

Alternatively, if using OAuth2 flow instead of API key, use accessToken instead of apiKey.

🤖 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` at line 52, The seeder is
creating empty Discord credentials because ClientAccessManager::make() is called
without parameters; update the 'credentials' value in the
PersonalDiscordServerSeeder to pass the encrypted bot token into
ClientAccessManager::make() (use Crypt::encrypt(config('discord.token')) as the
apiKey argument, or pass an accessToken param if you’re using OAuth2) so the
created credentials contain the token needed for authentication.

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

♻️ Duplicate comments (3)
app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php (3)

223-237: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject unsupported timeout durations before calling Discord.

When parseDuration() returns null, suspendMember() still sends communication_disabled_until => null. That turns an unknown duration into a silent success path and can effectively clear the timeout instead of applying one. Bail out before the PATCH when the duration is not recognized.

🤖 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 223 - 237, In suspendMember, reject unknown durations returned by
parseDuration instead of sending communication_disabled_until => null: check the
result of parseDuration($duration) (stored in $until) and if it is null, throw
or return an error/validation failure (do not call discordRequest), otherwise
proceed and pass $until->format(DateTimeInterface::ATOM) to discordRequest;
ensure the communication_disabled_until field is never sent as null and
reference suspendMember, parseDuration and discordRequest in the change.

149-155: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate the DM channel id before posting the message.

Both flows only check failed(). If Discord returns a 2xx with a malformed body, json('id') can be empty and we end up calling /channels//messages, silently dropping the notification. Guard the extracted id with blank() before building the URL.

Also applies to: 349-357

🤖 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 149 - 155, The DM channel id extracted via $dmResponse->json('id') is not
validated and can be empty even when response->failed() is false, causing a
request to /channels//messages; update the code around the discordRequest call
(the block invoking discordRequest and reading $dmResponse->json('id')) to guard
the extracted id with a blank() check (or equivalent non-empty validation)
before building the URL and only call discordRequest when the id is present;
apply the same validation to the other occurrence covering lines where
$dmResponse->json('id') is used (the 349-357 flow) so both flows skip or log an
error when the id is blank instead of posting to /channels//messages.

82-111: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate DM/delete side effects on a successful enforcement response.

For Mute / Suspend / Kick / Ban, this sends the DM and may delete the message before checking $response->failed(). A rejected Discord action can therefore still look successful to the user, and side-effect failures are swallowed while execute() still returns success. Keep Warn / ContentRemove as the intentional no-enforcement path, but move these side effects behind the successful-response check for real Discord actions.

🤖 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 82 - 111, Currently the DM send and original-message deletion occur before
verifying the Discord API call succeeded, allowing side effects for real
enforcement actions (Mute/Suspend/Kick/Ban) even when $response->failed();
modify the flow in DiscordModerationAdapter (methods: execute(),
sendDmNotification(), deleteOriginalMessage(), shouldDeleteContent()) so that
after obtaining $response you first check "if ($response instanceof Response &&
!$response->failed())" and only then perform sendDmNotification($token,
$discordId, $action) and deleteOriginalMessage($token, $action) (keeping the
try/catch suppression), while leaving Warn/ContentRemove paths unchanged so they
still skip enforcement logic.
🤖 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 283-289: The shouldDeleteContent(ActionType $type) predicate
currently only returns true for ActionType::Warn, ActionType::ContentRemove, and
ActionType::Suspend but the PR requires that Discord moderation actions delete
the original message and include it in the DM for Mute, Kick, and Ban as well;
update shouldDeleteContent to also return true for ActionType::Mute,
ActionType::Kick, and ActionType::Ban, and make the same change to the other
identical predicate/check referenced in the file (the second occurrence around
the moderation logic) so both places consistently treat Mute, Kick, and Ban as
actions that delete the original message and include it in the notification.
- Around line 443-450: The Log::warning call inside the response->failed()
branch is logging the raw $payload (which may contain sensitive moderation
content); change this to redact or remove sensitive fields before logging by
constructing a sanitizedPayload that strips or masks keys like "message",
"content", "dm", "reason", "user_id" (or only include safe metadata such as
payload size, truncated preview, and user count) and use that sanitizedPayload
in the Log::warning call for the DiscordModerationAdapter::send/HTTP request
failure handling so sensitive data is not persisted in logs.

---

Duplicate comments:
In `@app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php`:
- Around line 223-237: In suspendMember, reject unknown durations returned by
parseDuration instead of sending communication_disabled_until => null: check the
result of parseDuration($duration) (stored in $until) and if it is null, throw
or return an error/validation failure (do not call discordRequest), otherwise
proceed and pass $until->format(DateTimeInterface::ATOM) to discordRequest;
ensure the communication_disabled_until field is never sent as null and
reference suspendMember, parseDuration and discordRequest in the change.
- Around line 149-155: The DM channel id extracted via $dmResponse->json('id')
is not validated and can be empty even when response->failed() is false, causing
a request to /channels//messages; update the code around the discordRequest call
(the block invoking discordRequest and reading $dmResponse->json('id')) to guard
the extracted id with a blank() check (or equivalent non-empty validation)
before building the URL and only call discordRequest when the id is present;
apply the same validation to the other occurrence covering lines where
$dmResponse->json('id') is used (the 349-357 flow) so both flows skip or log an
error when the id is blank instead of posting to /channels//messages.
- Around line 82-111: Currently the DM send and original-message deletion occur
before verifying the Discord API call succeeded, allowing side effects for real
enforcement actions (Mute/Suspend/Kick/Ban) even when $response->failed();
modify the flow in DiscordModerationAdapter (methods: execute(),
sendDmNotification(), deleteOriginalMessage(), shouldDeleteContent()) so that
after obtaining $response you first check "if ($response instanceof Response &&
!$response->failed())" and only then perform sendDmNotification($token,
$discordId, $action) and deleteOriginalMessage($token, $action) (keeping the
try/catch suppression), while leaving Warn/ContentRemove paths unchanged so they
still skip enforcement logic.
🪄 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: 649f39b2-7669-44b4-8891-2fd6bab9ef56

📥 Commits

Reviewing files that changed from the base of the PR and between f37d6a6 and ebd1e34.

📒 Files selected for processing (3)
  • app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php
  • app-modules/moderation/src/Classification/Jobs/RouteDecision.php
  • app/Providers/Filament/HubPanelProvider.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • app-modules/moderation/src/Classification/Jobs/RouteDecision.php

Comment on lines +283 to +289
private function shouldDeleteContent(ActionType $type): bool
{
return in_array($type, [
ActionType::Warn,
ActionType::ContentRemove,
ActionType::Suspend,
], true);
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

shouldDeleteContent() does not match the PR’s moderation behavior.

The PR objective says Discord moderation actions delete the original message and include it in the DM. This predicate only enables that for Warn, ContentRemove, and Suspend, so Mute, Kick, and Ban leave the original message up and also skip including it in the notification.

Also applies to: 411-414

🤖 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 283 - 289, The shouldDeleteContent(ActionType $type) predicate currently
only returns true for ActionType::Warn, ActionType::ContentRemove, and
ActionType::Suspend but the PR requires that Discord moderation actions delete
the original message and include it in the DM for Mute, Kick, and Ban as well;
update shouldDeleteContent to also return true for ActionType::Mute,
ActionType::Kick, and ActionType::Ban, and make the same change to the other
identical predicate/check referenced in the file (the second occurrence around
the moderation logic) so both places consistently treat Mute, Kick, and Ban as
actions that delete the original message and include it in the notification.

Comment on lines +443 to +450
if ($response->failed()) {
Log::warning('Discord API request failed.', [
'method' => mb_strtoupper($method),
'url' => $url,
'payload' => $payload,
'status' => $response->status(),
'response' => $response->json() ?: $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 | ⚡ Quick win

Avoid logging raw moderation payloads on Discord failures.

$payload can contain the original moderated message, user ids, reasons, and DM content. Logging it verbatim on every failed request turns normal Discord delivery failures into long-lived retention of moderated content in application logs. Prefer redacting the payload or logging only safe metadata.

🤖 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 443 - 450, The Log::warning call inside the response->failed() branch is
logging the raw $payload (which may contain sensitive moderation content);
change this to redact or remove sensitive fields before logging by constructing
a sanitizedPayload that strips or masks keys like "message", "content", "dm",
"reason", "user_id" (or only include safe metadata such as payload size,
truncated preview, and user count) and use that sanitizedPayload in the
Log::warning call for the DiscordModerationAdapter::send/HTTP request failure
handling so sensitive data is not persisted in logs.

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.

4 participants