Feature/discord bot moderation#215
Conversation
…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.
…yload no ModerationContentDTO
…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
…ovos comportamentos
…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
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis 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
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ 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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
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 winThe pre-resolved author identity is dropped before reaching
IngestContent.
'author' => $authorIdentity?->useris passed intoDiscordModerationAdapter::ingest(), but the adapter'singest()(seeDiscordModerationAdapter.phplines 35-50) only forwards a fixed shape (content_id,content_type,author_external_id,text,media_urls,tenant_id,metadata) toModerationContentDTO::fromPlatform. Theauthorkey is never propagated to the DTO, soIngestContent::handle()ends up running the sameExternalIdentityquery again. Either:
- propagate
authorthroughingest()into the DTO so the lookup is short-circuited, or- drop the redundant
$authorIdentitylookup here and letIngestContenthandle resolution.Either way the duplicate query should go. Also consider adding a
model_typefilter to this query (and the one inIngestContent) to avoid matching identities not bound to aUser.🤖 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 winExtract the duplicated DM-channel creation logic.
notify()(lines 111-123) andsendDmNotification()(lines 211-236) bothPOST /users/@me/channelswith arecipient_id, checkfailed(), thenPOSTto/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 ornull. This also lets you guard against an emptyidin the response ((string) $dmResponse->json('id')currently coercesnullto'', 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()andsendDmNotification()can both call it and bail early onnull.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 winMake user/tenant creation idempotent.
The early-return check only verifies the
ExternalIdentityrow. If a previous seeder run created theUser(orTenant) but failed before inserting the identity, re-running will hit unique-constraint violations onusers.email/users.username. PreferfirstOrCreatefor 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
⛔ Files ignored due to path filters (2)
composer.lockis excluded by!**/*.lockpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (17)
app-modules/bot-discord/src/BotDiscordServiceProvider.phpapp-modules/bot-discord/src/Events/MessageReceivedEvent.phpapp-modules/bot-discord/src/Moderation/DiscordModerationAdapter.phpapp-modules/bot-discord/tests/Feature/Moderation/DiscordModerationAdapterTest.phpapp-modules/moderation/lang/en/notifications.phpapp-modules/moderation/lang/pt_BR/notifications.phpapp-modules/moderation/src/Classification/Jobs/ClassifyContent.phpapp-modules/moderation/src/Classification/Jobs/IngestContent.phpapp-modules/moderation/src/Classification/Jobs/RouteDecision.phpapp-modules/moderation/src/DTOs/ModerationContentDTO.phpapp-modules/moderation/tests/Feature/Classification/RouteDecisionTest.phpapp-modules/moderation/tests/Unit/PenaltyAdvisorTest.phpbootstrap/providers.phpcomposer.jsonconfig/discord.phpconfig/services.phpdatabase/seeders/PersonalDiscordServerSeeder.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+.
…e remover HubPanelProvider prematuro
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
app-modules/bot-discord/src/Events/MessageReceivedEvent.phpapp-modules/bot-discord/src/Moderation/DiscordModerationAdapter.phpapp-modules/moderation/src/Classification/Jobs/IngestContent.phpapp-modules/moderation/src/DTOs/ModerationContentDTO.phpapp-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
…ter para evitar erros
There was a problem hiding this comment.
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
📒 Files selected for processing (1)
app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php
…lize em ModerationContentDTO
…ar o proprietário existente e aprimorar a lógica de busca do autor em IngestContent
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
app-modules/moderation/tests/Feature/Enforcement/ExecuteActionTest.php (1)
95-110: ⚡ Quick winExpand 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
⛔ Files ignored due to path filters (1)
composer.lockis excluded by!**/*.lock
📒 Files selected for processing (6)
PR.mdapp-modules/moderation/src/Classification/Actions/Advisors/HistoryBasedPenaltyAdvisor.phpapp-modules/moderation/src/Classification/Jobs/IngestContent.phpapp-modules/moderation/src/DTOs/ModerationContentDTO.phpapp-modules/moderation/tests/Feature/Enforcement/ExecuteActionTest.phpdatabase/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(); |
There was a problem hiding this comment.
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.
| $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(), |
There was a problem hiding this comment.
🧩 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:
- 1: https://docs.discord.com/developers/topics/oauth2
- 2: https://github.com/he4rt/He4rt-Bot
- 3: https://github.com/he4rt/he4rt-bot-next
🏁 Script executed:
# Find ClientAccessManager class definition
find . -type f -name "*.php" | xargs grep -l "class ClientAccessManager" 2>/dev/null | head -20Repository: he4rt/heartdevs.com
Length of output: 136
🏁 Script executed:
# Search for ClientAccessManager usage throughout codebase
rg "ClientAccessManager" --type php -A 2 -B 2Repository: 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 2Repository: 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 -20Repository: he4rt/heartdevs.com
Length of output: 1229
🏁 Script executed:
cat -n ./app-modules/identity/src/ExternalIdentity/Data/ClientAccessManager.php | head -100Repository: 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.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php (3)
223-237:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReject unsupported timeout durations before calling Discord.
When
parseDuration()returnsnull,suspendMember()still sendscommunication_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 winValidate 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 withblank()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 winGate 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 whileexecute()still returns success. KeepWarn/ContentRemoveas 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
📒 Files selected for processing (3)
app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.phpapp-modules/moderation/src/Classification/Jobs/RouteDecision.phpapp/Providers/Filament/HubPanelProvider.php
🚧 Files skipped from review as they are similar to previous changes (1)
- app-modules/moderation/src/Classification/Jobs/RouteDecision.php
| private function shouldDeleteContent(ActionType $type): bool | ||
| { | ||
| return in_array($type, [ | ||
| ActionType::Warn, | ||
| ActionType::ContentRemove, | ||
| ActionType::Suspend, | ||
| ], true); |
There was a problem hiding this comment.
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.
| 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(), | ||
| ]); |
There was a problem hiding this comment.
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.
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
WebModerationAdapterestava 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:DiscordModerationAdapterNovo adapter que implementa
ModerationPlatformContracte executa ações reais via Discord REST API:WarnMute/SuspendPATCH /guilds/{id}/members/{user}communication_disabled_untilKickDELETE /guilds/{id}/members/{user}BanPUT /guilds/{id}/bans/{user}Para todas as ações:
ExecutionResultDTO::failure()sem crashar o fluxoMelhorias na pipeline de classificação
ClassifyContent: regras manuais têm prioridade sobre a IA — a OpenAI só é chamada quando nenhuma regra bate na mensagemIngestContent: resolve o autor porexternal_identityquando não vem diretamente no payload do eventoRouteDecision: preserva a ação sugerida por regra manual ao atualizar a prioridade do casoTraduções
Strings do embed de DM adicionadas no namespace
moderation::notifications.discord_dm.*paraenept_BR.Seeder para desenvolvimento local
PersonalDiscordServerSeedercria automaticamente o tenant, usuário owner eexternal_identityvinculada aoHE4RT_DISCORD_GUILDconfigurado no.env, facilitando o setup do ambiente local com o bot.Arquivos alterados
app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.phpapp-modules/bot-discord/tests/Feature/Moderation/DiscordModerationAdapterTest.phpapp-modules/bot-discord/src/Events/MessageReceivedEvent.phpapp-modules/bot-discord/src/BotDiscordServiceProvider.phpapp-modules/moderation/src/ModerationServiceProvider.phpapp-modules/moderation/src/Classification/Jobs/ClassifyContent.phpapp-modules/moderation/src/Classification/Jobs/IngestContent.phpapp-modules/moderation/src/Classification/Jobs/RouteDecision.phpapp-modules/moderation/src/DTOs/ModerationContentDTO.phpapp-modules/moderation/lang/en/notifications.phpapp-modules/moderation/lang/pt_BR/notifications.phpdatabase/seeders/PersonalDiscordServerSeeder.phpconfig/discord.phpdiscord.tokenconfig/services.phpopenai.api_keyCobertura de testes
Os 20 testes do
DiscordModerationAdapterTestcobrem:24h,7de28d(limite máximo do Discord)24h,7d,permanent)notify()— abre canal DM e envia mensagemresolveUser()— resolve usuário por Discord ID viaexternal_identitiesingest()— transforma payload de evento emModerationContentDTO999d) — timeout nulo, sem crashVariáveis de ambiente necessárias
Como testar o bot: