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
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository YAML (base), Central YAML (inherited) Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis PR integrates Discord message moderation by introducing a 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
| $this->sendDmNotification($token, $discordId, $action); | ||
|
|
||
| try { | ||
| $this->deleteOriginalMessage($token, $action); | ||
| } catch (Throwable) { | ||
| // message may have already been deleted or case snapshot is missing | ||
| } | ||
|
|
||
| if (!$response instanceof Response) { | ||
| return ExecutionResultDTO::success(Platform::Discord, ['action' => $action->action_type->value]); | ||
| } | ||
|
|
||
| if ($response->failed()) { | ||
| return ExecutionResultDTO::failure(Platform::Discord, $response->body()); |
There was a problem hiding this comment.
Validate the enforcement call before firing notification/deletion side effects.
execute() sends the DM and attempts message deletion before it checks $response->failed(), and both helpers hide their own HTTP result from the caller. A rejected kick/ban/mute can therefore still notify the user as if the action happened, and partial Discord-side failures stay invisible to the pipeline. Check the main moderation response first, then have the side-effect helpers return Response|null so their outcome can be accounted for too.
Also applies to: 207-218, 220-252
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php` around
lines 76 - 89, The execute() flow currently calls sendDmNotification() and
deleteOriginalMessage() before validating the main enforcement response, which
can produce misleading side-effects; change execute() (the method handling
$response) to first verify that $response is a Response and not
$response->failed() and only then invoke sendDmNotification() and
deleteOriginalMessage(); refactor those helpers (sendDmNotification and
deleteOriginalMessage) to return Response|null (or a result object) instead of
hiding HTTP results so callers can log/handle failures, and update execute() to
inspect those returned values and propagate failure via ExecutionResultDTO when
any side-effect response indicates failure; apply the same pattern to the other
affected blocks (the other execute()/enforcement branches referenced).
| private function timeoutMember(string $token, string $guildId, string $discordId, ?string $duration): Response | ||
| { | ||
| $until = $this->parseDuration($duration); | ||
|
|
||
| return $this->http($token) | ||
| ->patch(sprintf('https://discord.com/api/v10/guilds/%s/members/%s', $guildId, $discordId), [ | ||
| 'communication_disabled_until' => $until?->format(DateTimeInterface::ATOM), | ||
| ]); |
There was a problem hiding this comment.
Reject unsupported timeout durations before calling Discord.
parseDuration() falls back to null, and timeoutMember() still issues the PATCH. That makes a typo/new duration value a silent success path for Mute/Suspend instead of a hard failure. Treat null as invalid here and bail out early.
Suggested guard
private function timeoutMember(string $token, string $guildId, string $discordId, ?string $duration): Response
{
$until = $this->parseDuration($duration);
+ if ($until === null) {
+ throw new \InvalidArgumentException('Unsupported Discord timeout duration.');
+ }
return $this->http($token)
->patch(sprintf('https://discord.com/api/v10/guilds/%s/members/%s', $guildId, $discordId), [
'communication_disabled_until' => $until?->format(DateTimeInterface::ATOM),
]);Also applies to: 197-203
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app-modules/bot-discord/src/Moderation/DiscordModerationAdapter.php` around
lines 167 - 174, parseDuration() can return null and timeoutMember() currently
proceeds to call Discord anyway; add an explicit guard in timeoutMember(string
$token, string $guildId, string $discordId, ?string $duration) to treat a null
$until as an invalid duration and bail out early (return a 4xx/invalid input
response or throw a clear exception) instead of calling
$this->http(...)->patch(...); do the same for the other method that uses
parseDuration and sets 'communication_disabled_until' (the same parseDuration
usage around lines 197-203) so no PATCH is issued when the duration is invalid.
…lize em ModerationContentDTO
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