Instruction file imported from igorquintaes/BotDeScans (
.github/instructions/publish-feature.instructions.md). Copyright stays with the author.
---
applyTo: "BotDeScans.App/Features/Publish/**"
description: >
Skill (conhecimento + diretrizes) para evoluir e revisar a feature
Features/Publish, responsável por orquestrar o pipeline de publicação
de capítulos: download dos arquivos, processamento (compressão, ZIP, PDF),
upload em provedores externos (Mega, Box, Google Drive, MangaDex,
Sakura Mangás) e publicação no Blogger, com feedback em tempo real no
Discord.
Skill — Features/Publish
Esta skill descreve como o pipeline de publicação funciona hoje, quais são os contratos esperados ao adicionar/alterar passos, e os trade-offs conhecidos da arquitetura atual. Use-a como base obrigatória ao revisar PRs ou propor refatorações nesta feature.
1. Visão geral arquitetural
A feature segue o padrão Vertical Slice + Pipeline of Steps dentro do
projeto BotDeScans.App. Está dividida em duas sub-slices:
| Pasta | Responsabilidade |
|---|---|
Publish/Command/ |
Slash-command /publish que abre uma Modal Discord para o usuário preencher os dados do capítulo. |
Publish/Interaction/ |
Recebe o submit da modal, monta o estado e executa o pipeline de passos. |
DI é registrada de forma autossuficiente por slice via arquivos
+Dependencies.cs (AddPublishServices ? AddCommands + AddInteractions
AddPublishSteps+AddPings). Todos os tipos sãoScoped, exceto pings sem estado (EveryonePing,NonePing) que sãoSingleton. O escopo do contêiner casa com o ciclo de vida de uma interação Discord, então oStatecompartilhado vive apenas durante uma execução do comando.
2. Fluxo de execução (alto nível)
/publish (Commands.cs)
??? Modal "Features.Publish"
??? Interactions.ExecuteAsync (submit)
1. Preenche State.ChapterInfo
2. State.Steps = StepsService.GetEnabledSteps()
3. Handler.ExecuteAsync(ct)
?? ManagementSteps ? Execute (em ordem)
?? PublishSteps ? Validate (todos antes de qualquer execute)
?? PublishSteps ? Execute (em ordem)
4. Sucesso ? DiscordPublisher.SuccessReleaseMessageAsync
Falha ? DiscordPublisher.ErrorReleaseMessageAsync
O Handler mantém o invariante crítico: se algum passo falhar, a cadeia
para imediatamente (if (result.IsFailed) break;). O DiscordPublisher
é chamado entre cada passo para atualizar a mesma embed (a "tracking
message") refletindo StepStatus de cada StepInfo.
3. Modelo de domínio do pipeline
3.1. Hierarquia de Steps (Steps/IStep.cs)
IStep // ExecuteAsync, Name (StepName), Type (StepType)
?? IManagementStep // + IsMandatory
?? IPublishStep // + Dependency (StepName?), ValidateAsync
IManagementStep= passos internos (Setup, Download, Compress, Zip, Pdf). Não têm validação prévia; os marcados comoIsMandatory = trueentram no pipeline mesmo que ausentes na configuração.IPublishStep= passos que falam com mundo externo (uploads, publicação no Blogger). PossuemValidateAsync(chamado em fase separada, antes de qualquer execute) e podem declarar umaDependency(outroStepName) que será automaticamente incluída.
3.2. Seleção de passos (StepsService.GetEnabledSteps)
União distinta e ordenada por StepName de:
- Configuração —
Settings:Publish:Steps(lista deStepName). - Dependências — para cada
IPublishStepselecionado, inclui oStepNamedeclarado emDependency(ex.:UploadZipMegapuxaZipFiles). - Mandatórios — todos os
IManagementStepcomIsMandatory = true.
O resultado é encapsulado em EnabledSteps : ReadOnlyDictionary<IStep, StepInfo>,
que expõe ManagementSteps, PublishSteps, MessageStatus, Details,
ColorStatus. Esse modelo é o "view-model" da embed do Discord.
3.3. Estado compartilhado (State)
Bag mutável (todos set) com:
Steps—EnabledSteps.Title— entidade de banco preenchida noSetupStep.ChapterInfo— DTO da modal.ReleaseLinks— record de URLs por destino, descobertas conforme cada upload termina; usado emLinksfields da embed final e peloTextReplacer.InternalData— paths de arquivos intermediários (OriginContentFolder,CoverFilePath,ZipFilePath,PdfFilePath,BloggerImageAsBase64,BoxPdfReaderKey,Pings).
StateValidator (FluentValidation) é executado dentro do
SetupStep e é o lugar onde regras cross-step vivem (ex.: exigir
ExternalReference.MangaDex se UploadMangaDexStep está habilitado;
exigir role configurada se PingType.Global).
3.4. Status (StepStatus)
QueuedForValidation ? QueuedForExecution ? Success
? Error
? Skip (vindo de Title.SkipSteps)
StepInfo.UpdateStatus aplica a transição automática usando o
Result do passo. Pulos vêm de Title.SkipSteps (persistidos por
título) e são checados em runtime no Handler.ShouldSkip.
3.5. Pings (Pings/)
Estratégia poliformica selecionada por Settings:Publish:PingType
(None, Everyone, Global, Role). SetupStep resolve o Ping
via pings.Single(x => x.IsApplicable) e armazena o texto em
InternalData.Pings, consumido pelo DiscordPublisher na mensagem
final do canal de release.
3.6. Substituição de texto (TextReplacer)
Templating manual com tags !##KEY##! e blocos
!##START_REMOVE_IF_EMPTY_KEY##! ... !##END_REMOVE_IF_EMPTY_KEY##!
removidos quando o valor correspondente é vazio. Usado para o post do
Blogger e para state.ChapterInfo.Message na embed de release.
4. Convenções obrigatórias ao alterar/criar Steps
- Nome do arquivo prefixado por número (
NN_NomeStep.cs) só para ordenação visual; a ordem real de execução é porStepName(OrderBy(step => step.Name)). Se você adicionar um step novo, escolha o valor numérico deStepNamedeliberadamente — ele é persistido em banco (SkipStep) e não pode ser alterado depois. - Registre o step em
Steps/+Dependencies.cs(interfaceIStep). - Adicione a descrição em
StepName(vai para a embed) e o emoji correspondente emStepStatus/atributoEmoji. - Para passos com IO externo: implemente
IPublishStepe useValidateAsyncpara falhar cedo (antes de qualquer upload). Não faça side-effects emValidateAsync. - Se o passo consome um arquivo gerado por outro (ex.: ZIP/PDF),
declare
Dependencypara garantir inclusão automática. - Toda IO/dependência externa deve retornar
FluentResults.Result; exceções são capturadas noIStep.SafeCallAsync(extension emObjectExtensions) e convertidas em erro genérico com log Serilog. - Mutar o
Stateapenas viastate.InternalData.*(artefatos) oustate.ReleaseLinks.*(URLs). Não toque emTitle/ChapterInfoapósSetupStep. - Marque com
[ExcludeFromCodeCoverage]apenas wrappers de infraestrutura (já é a convenção dos+Dependencies.cse doDiscordPublisher).
5. Pontos de extensão típicos
- Novo destino de upload ? criar
IPublishStep, adicionar entry emLinks(com[Description]), emTextReplacer.ReplaceRules, emStepName(novo valor numérico) e DI. - Nova validação cross-step ?
StateValidatorcom.When(...)observandostate.Steps. - Novo tipo de Ping ? herdar
Ping, registrar emPings/+Dependenciese adicionar valor emPingType.
Avaliação da implementação atual
? Pontos fortes
- Vertical slice bem isolado. Toda a feature, incluindo DI,
contratos, modelos e UI Discord, vive sob
Features/Publish.+Dependencies.cspor pasta evita o "DI hell" central. - Pipeline declarativo via DI. Adicionar passo = registrar uma
classe e ajustar
StepName. OHandleré agnóstico do conteúdo dos passos. - Separação Validate/Execute para passos externos evita gastar tempo/banda fazendo upload parcial quando há erro de configuração (referência ausente, role inexistente, etc.). É um padrão correto de "fail-fast" para IO caro.
- Feedback incremental no Discord (
DiscordPublisher.UpdateTrackingMessageAsyncchamado entre cada passo) — UX excelente para um processo longo. SafeCallAsyncgarante que exceções não derrubam o bot inteiro e ainda compõem umResultrastreável.StepNamenumérico estável permite persistirSkipSteppor título (Title.SkipSteps) sem acoplar ao nome do tipo C#.- Pings via Strategy +
IsApplicableé simples e extensível.
?? Pontos de atenção / smells
Stateé uma bag mutável compartilhada por todos os steps. Qualquer step pode escrever qualquer campo; o "contrato" entre steps (ex.:ZipFilesStepproduzZipFilePath,UploadZipMegao consome) só existe por convenção e via!(null-forgiving). Isso:- dificulta detectar dependências reais (apenas
StepName.Dependencydeclara, mas nem todo consumo está mapeado); - impede paralelizar passos independentes com segurança;
- torna testes unitários frágeis (precisa preparar um
Statecompleto).
- dificulta detectar dependências reais (apenas
Handlermistura duas estratégias de iteração. Constrói umIEnumerable<Func<Task<Result>>>comUnion, depois usaforeachcomif (IsFailed) break;. Funciona, mas oResult.Mergeno laço só agrega o último — o "merge" perde os erros anteriores quando há curto-circuito. A intenção é clara, mas o código é difícil de evoluir (ex.: política de "continuar mesmo com erro" paraUploadSakuraMangasStep— vide TODO no próprio arquivo).Validateantes de qualquerExecuteé bom, mas hoje todas as implementações deValidateAsyncretornamResult.Ok(). O contrato está pago em complexidade sem pagar dividendo. Ou se preenche, ou se considera remover/condensar.StepsServicepercorreIEnumerable<IStep>3 vezes e usaUnion+DistinctBypara deduplicar. Funciona, mas a resolução de dependências é rasa (1 nível): se um dependente declarar uma dependência que ela mesma tem dependência, não é resolvido em cascata. Hoje não há esse caso, mas é uma armadilha futura.DiscordPublishermantémtrackingMessagecomo campo mutável (estado implícito). Fora do escopo (Scoped DI), funciona; mas é uma fonte de bug se alguém reutilizar a instância. Marcado como[ExcludeFromCodeCoverage], então não há testes que defendam o invariante.TextReplacer.Replaceé O(N×M) sobre o tamanho do template e regras, com múltiplostext.Replace/IndexOfpor chave. Para um template de Blogger isso é irrelevante; só vira problema se o uso crescer.+Dependencies.csemPingsregistraSingletoneScopedno mesmoIEnumerable<Ping>. ORolePing(Scoped) injetaState(Scoped), o que está correto, mas registrar pings stateless comoSingletone stateful comoScopedem um mesmoIEnumerableexige cuidado —IsApplicablelêIConfigurationestaticamente em todos, o que está OK, mas se um dia for preciso chamarpingsfora de um escopo, o resolve quebra silenciosamente.ShouldSkipé checado dentro doHandlere emValidateAsynctambém marcaSetToSkip, mas o execute usaResult.Ok()(sem atualizar status), porque a marcação já foi feita no validate. Funciona, mas a regra "skip" está espalhada — concentrá-la num único ponto (ex.: ao montarEnabledSteps) deixaria mais óbvio.- Acoplamento direto ao Google Drive em
DownloadStep. O nome sugere genérico, mas só sabe baixar de Drive. Se um dia houver outra origem (Mega, S3 etc.), vira problema. Renomear paraDownloadGoogleDriveStepou abstrair atrás de umIContentSource. Result.Merge(result, await execStep())acumulaReasons, mas como o laço quebra no primeiro fail, na prática só transporta 1 erro. Se o objetivo é coletar warnings/erros agregados, falta um canal separado.
?? Abordagens alternativas
A) Pipeline tipado com input/output explícitos (estilo Result Pipeline)
Cada step declara TIn/TOut:
public interface IPipelineStep<TIn, TOut> {
Task<Result<TOut>> ExecuteAsync(TIn input, CancellationToken ct);
}
E o pipeline é montado em compile-time (A ? B ? C), com cada estágio
recebendo o output do anterior.
- + Elimina a bag mutável; dependências viram tipos.
- + Compila-se a topologia; passos opcionais ficam evidentes.
- ? Muito mais cerimônia em C# (genéricos aninhados, builder específico).
- ? Difícil acomodar passos opcionais/paralelos (Mega e Box ao mesmo tempo) sem virar um DAG.
B) MediatR / behaviors com pipeline behaviors
Cada step vira IRequest/IRequestHandler, e o orquestrador envia
requests em sequência. Validation é um IPipelineBehavior.
- + Padrão muito conhecido na comunidade .NET.
- + Logging/telemetria via behaviors transversal.
- ? Para um único caso de uso linear é overkill — adiciona indireção sem reduzir o problema do estado compartilhado.
- ? O feedback "embed atualizada entre passos" não casa bem com o modelo request/response do MediatR.
C) Workflow engine (Elsa, MassTransit Sagas, Temporal/Workflows .NET, Hangfire continuations)
- + Persistência do estado, retry/replay automáticos, paralelismo e DAG de dependências reais. Cada upload poderia ser uma activity resiliente.
- + Resolveria o problema de retomar uma publicação que falhou no meio (hoje: refazer tudo).
- ? Custo operacional alto (storage, dashboard, versionamento de workflow).
- ? Acoplar feedback em tempo real ao Discord exige callbacks/ eventos da engine — nem todas suportam bem.
D) DAG com paralelismo controlado (mantendo o IStep atual)
Manter IStep/IPublishStep mas substituir o Handler por um
scheduler que respeita Dependency como aresta de um grafo:
-
Ms (Setup ? Download ? Compress) sequencial.
-
ZipFilesePdfFilesem paralelo (não dependem entre si). -
Cada
UploadXparalelo a outros uploads que compartilham a mesma dependência (ZipFilesouPdfFiles). -
+ Ganho real de throughput (uploads para Mega/Box/Drive são IO-bound).
-
+ Mantém o modelo mental atual (steps, status, embed).
-
? Exige tornar
Statethread-safe ou particionar por destino (Links/InternalDatacampos atômicos por upload já ajudam). -
? Atualização de embed precisa serializar (lock) para evitar rate-limit e race-condition na
trackingMessage.
E) Imutabilizar o State via "context per stage"
Trocar a bag mutável por um record imutável PublishContext que cada
step transforma e devolve. Equivalente a uma versão "leve" de (A) sem
genéricos:
public interface IStep {
Task<Result<PublishContext>> ExecuteAsync(PublishContext ctx, CancellationToken ct);
}
- + Testes ficam triviais; imutabilidade documenta o que cada passo produz.
- + Permite "dry-run" / simulação fácil.
- ?
Links/InternalDataviram cópias — overhead irrisório aqui, mas mais códigowith { ... }. - ? Refator grande para todos os 14 steps.
6. Checklist de PR para esta feature
- Novo
StepNametem valor numérico único e final. - Step registrado em
Steps/+Dependencies.cs. -
DescriptionnoStepNameeEmojicorrespondente emStepStatus(se status novo). -
Dependencydeclarada se consomeZipFilePath/PdfFilePath/etc. -
ValidateAsyncfaz checagens sem side-effects. - Mutação de
Stateapenas emInternalData/ReleaseLinks. - Novo provedor externo: cobrir com
Resulte mapear exceções conhecidas emFluentResultsExtensions.GetErrorsInfo. - Atualizar
Links+TextReplacer.ReplaceRulesse houver nova URL. - Atualizar
StateValidatorse a presença do step exige umaExternalReferenceou configuração. - Testes unitários cobrindo o novo step (StepInfo + Result).