Untitled

Anonymous
plain_text
02/18/2026 3:12 PM
22.2 KB
30
Indexable
package com.example.discord.templates;

import net.dv8tion.jda.api.components.MessageTopLevelComponent;
import net.dv8tion.jda.api.components.utils.ComponentDeserializer;
import net.dv8tion.jda.api.components.utils.ComponentSerializer;
import net.dv8tion.jda.api.utils.FileUpload;
import net.dv8tion.jda.api.utils.data.DataArray;
import net.dv8tion.jda.api.utils.data.DataObject;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Processa templates de Components V2 do JDA:
 * - Desserializa JSON -> componentes JDA (1 vez)
 * - Renderiza substituindo placeholders {variavel} em campos textuais
 * - Re-serializa quando necessário
 *
 * Tudo isso usando as classes OFICIAIS do JDA:
 * - ComponentDeserializer / ComponentSerializer
 * - DataArray / DataObject
 */
public final class JdaMessageTemplateProcessor {

    private final PlaceholderEngine placeholderEngine;
    private final TextFieldPolicy textFieldPolicy;

    public JdaMessageTemplateProcessor(PlaceholderEngine placeholderEngine, TextFieldPolicy textFieldPolicy) {
        this.placeholderEngine = Objects.requireNonNull(placeholderEngine, "placeholderEngine");
        this.textFieldPolicy = Objects.requireNonNull(textFieldPolicy, "textFieldPolicy");
    }

    /**
     * Desserializa o JSON (array de components top-level) para uma estrutura de template.
     * Você faz isso UMA VEZ e guarda o TemplateMessageComponents.
     */
    public TemplateMessageComponents deserializeTemplate(
            String jsonArrayComponents,
            List<FileUpload> implicitFileUploads
    ) {
        Objects.requireNonNull(jsonArrayComponents, "jsonArrayComponents");
        Objects.requireNonNull(implicitFileUploads, "implicitFileUploads");

        DataArray rawComponentsDataArray = DataArray.fromJson(jsonArrayComponents);

        ComponentDeserializer componentDeserializer = new ComponentDeserializer(implicitFileUploads);
        List<MessageTopLevelComponent> components = componentDeserializer
                .deserializeAs(MessageTopLevelComponent.class, rawComponentsDataArray)
                .toList();

        // Descobrir placeholders existentes no template (uma vez só)
        Set<String> placeholdersFound = placeholderEngine.findPlaceholders(rawComponentsDataArray, textFieldPolicy);

        return new TemplateMessageComponents(
                List.copyOf(components),
                List.copyOf(implicitFileUploads),
                placeholdersFound
        );
    }

    /**
     * Renderiza o template substituindo placeholders com valores.
     * Retorna os componentes prontos + relatório de placeholders (usados, faltantes, etc).
     */
    public RenderedMessageComponents renderTemplate(
            TemplateMessageComponents template,
            Map<String, String> placeholderValues
    ) {
        Objects.requireNonNull(template, "template");
        Objects.requireNonNull(placeholderValues, "placeholderValues");

        // Serializa os componentes em memória (sem JSON string) para DataArray
        ComponentSerializer componentSerializer = new ComponentSerializer();
        componentSerializer.addAll(template.components());

        DataArray serializedDataArray = componentSerializer.toData();

        // Aplica substituição somente em campos textuais permitidos
        DataArray substitutedDataArray = placeholderEngine.applySubstitutions(
                serializedDataArray,
                placeholderValues,
                textFieldPolicy
        );

        // Desserializa de volta para objetos do JDA
        ComponentDeserializer componentDeserializer = new ComponentDeserializer(template.implicitFileUploads());
        List<MessageTopLevelComponent> renderedComponents = componentDeserializer
                .deserializeAs(MessageTopLevelComponent.class, substitutedDataArray)
                .toList();

        PlaceholderReport report = PlaceholderReport.from(template.placeholdersFoundInTemplate(), placeholderValues);

        return new RenderedMessageComponents(List.copyOf(renderedComponents), report);
    }

    /**
     * Serializa componentes prontos para JSON (se você precisar armazenar ou depurar).
     */
    public String serializeToJson(List<MessageTopLevelComponent> components) {
        Objects.requireNonNull(components, "components");

        ComponentSerializer componentSerializer = new ComponentSerializer();
        componentSerializer.addAll(components);

        return componentSerializer.toData().toJson();
    }

    // ======================================================================
    // DTOs (resultados) — imutáveis
    // ======================================================================

    public record TemplateMessageComponents(
            List<MessageTopLevelComponent> components,
            List<FileUpload> implicitFileUploads,
            Set<String> placeholdersFoundInTemplate
    ) {
        public TemplateMessageComponents {
            Objects.requireNonNull(components, "components");
            Objects.requireNonNull(implicitFileUploads, "implicitFileUploads");
            Objects.requireNonNull(placeholdersFoundInTemplate, "placeholdersFoundInTemplate");
        }
    }

    public record RenderedMessageComponents(
            List<MessageTopLevelComponent> components,
            PlaceholderReport placeholderReport
    ) {
        public RenderedMessageComponents {
            Objects.requireNonNull(components, "components");
            Objects.requireNonNull(placeholderReport, "placeholderReport");
        }
    }

    public record PlaceholderReport(
            Set<String> placeholdersFoundInTemplate,
            Set<String> placeholdersProvided,
            Set<String> placeholdersMissingValues,
            Set<String> placeholdersUnusedValues
    ) {
        public static PlaceholderReport from(Set<String> placeholdersFoundInTemplate, Map<String, String> placeholderValues) {
            Objects.requireNonNull(placeholdersFoundInTemplate, "placeholdersFoundInTemplate");
            Objects.requireNonNull(placeholderValues, "placeholderValues");

            Set<String> provided = new LinkedHashSet<>(placeholderValues.keySet());

            Set<String> missing = new LinkedHashSet<>(placeholdersFoundInTemplate);
            missing.removeAll(provided);

            Set<String> unused = new LinkedHashSet<>(provided);
            unused.removeAll(placeholdersFoundInTemplate);

            return new PlaceholderReport(
                    Collections.unmodifiableSet(new LinkedHashSet<>(placeholdersFoundInTemplate)),
                    Collections.unmodifiableSet(provided),
                    Collections.unmodifiableSet(missing),
                    Collections.unmodifiableSet(unused)
            );
        }
    }

    // ======================================================================
    // Abstrações SOLID (DIP/OCP)
    // ======================================================================

    /**
     * Decide quais campos são texto e podem sofrer substituição.
     * Evita substituir em campos indevidos e mantém o sistema estável.
     */
    public interface TextFieldPolicy {
        boolean isTextFieldName(String fieldName);
    }

    /**
     * Engine de placeholders: encontra e substitui {variavel} em campos textuais.
     */
    public interface PlaceholderEngine {
        Set<String> findPlaceholders(DataArray componentsDataArray, TextFieldPolicy textFieldPolicy);

        DataArray applySubstitutions(DataArray componentsDataArray,
                                     Map<String, String> placeholderValues,
                                     TextFieldPolicy textFieldPolicy);
    }

    // ======================================================================
    // Implementações padrão
    // ======================================================================

    /**
     * Política padrão: whitelist de chaves textuais comuns em componentes V2.
     */
    public static final class DefaultTextFieldPolicy implements TextFieldPolicy {

        private final Set<String> allowedTextFields;

        public DefaultTextFieldPolicy() {
            this.allowedTextFields = Set.of(
                    "content",      // TextDisplay
                    "label",        // Button, etc.
                    "placeholder",  // selects/inputs (se você usar)
                    "title",        // alguns itens
                    "description"   // MediaGalleryItem etc.
            );
        }

        @Override
        public boolean isTextFieldName(String fieldName) {
            return allowedTextFields.contains(fieldName);
        }
    }

    /**
     * Placeholders no formato {nome}.
     * - Não explode se faltar valor: mantém "{nome}" no texto
     * - Permite relatório de placeholders faltantes
     */
    public static final class CurlyBracesPlaceholderEngine implements PlaceholderEngine {

        private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{([a-zA-Z0-9_\\-]+)\\}");

        @Override
        public Set<String> findPlaceholders(DataArray componentsDataArray, TextFieldPolicy textFieldPolicy) {
            Objects.requireNonNull(componentsDataArray, "componentsDataArray");
            Objects.requireNonNull(textFieldPolicy, "textFieldPolicy");

            Set<String> placeholders = new LinkedHashSet<>();
            scanNodeForPlaceholders(componentsDataArray, textFieldPolicy, placeholders);
            return Collections.unmodifiableSet(placeholders);
        }

        @Override
        public DataArray applySubstitutions(DataArray componentsDataArray,
                                            Map<String, String> placeholderValues,
                                            TextFieldPolicy textFieldPolicy) {
            Objects.requireNonNull(componentsDataArray, "componentsDataArray");
            Objects.requireNonNull(placeholderValues, "placeholderValues");
            Objects.requireNonNull(textFieldPolicy, "textFieldPolicy");

            DataArray deepCopy = deepCopyArray(componentsDataArray);
            substituteNode(deepCopy, placeholderValues, textFieldPolicy);
            return deepCopy;
        }

        // ------------------------------
        // Scan
        // ------------------------------

        private void scanNodeForPlaceholders(Object node,
                                             TextFieldPolicy textFieldPolicy,
                                             Set<String> placeholders) {
            if (node instanceof DataArray array) {
                for (int i = 0; i < array.length(); i++) {
                    scanNodeForPlaceholders(array.get(i), textFieldPolicy, placeholders);
                }
                return;
            }

            if (node instanceof DataObject object) {
                for (String key : object.keys()) {
                    Object value = object.get(key);

                    if (value instanceof String text && textFieldPolicy.isTextFieldName(key)) {
                        collectPlaceholders(text, placeholders);
                    } else {
                        scanNodeForPlaceholders(value, textFieldPolicy, placeholders);
                    }
                }
            }
        }

        private void collectPlaceholders(String text, Set<String> placeholders) {
            Matcher matcher = PLACEHOLDER_PATTERN.matcher(text);
            while (matcher.find()) {
                placeholders.add(matcher.group(1));
            }
        }

        // ------------------------------
        // Substitute
        // ------------------------------

        private void substituteNode(Object node,
                                    Map<String, String> placeholderValues,
                                    TextFieldPolicy textFieldPolicy) {

            if (node instanceof DataArray array) {
                for (int i = 0; i < array.length(); i++) {
                    Object element = array.get(i);
                    substituteNode(element, placeholderValues, textFieldPolicy);
                }
                return;
            }

            if (node instanceof DataObject object) {
                for (String key : object.keys()) {
                    Object value = object.get(key);

                    if (value instanceof String text && textFieldPolicy.isTextFieldName(key)) {
                        String replaced = replacePlaceholders(text, placeholderValues);
                        object.put(key, replaced);
                        continue;
                    }

                    substituteNode(value, placeholderValues, textFieldPolicy);
                }
            }
        }

        private String replacePlaceholders(String template, Map<String, String> placeholderValues) {
            Matcher matcher = PLACEHOLDER_PATTERN.matcher(template);
            StringBuilder result = new StringBuilder(template.length());

            int lastIndex = 0;
            while (matcher.find()) {
                result.append(template, lastIndex, matcher.start());

                String key = matcher.group(1);
                String replacement = placeholderValues.get(key);

                // Se não houver valor, mantém o placeholder intacto
                result.append(replacement != null ? replacement : matcher.group(0));

                lastIndex = matcher.end();
            }

            result.append(template, lastIndex, template.length());
            return result.toString();
        }

        // ------------------------------
        // Deep copy (DataObject/DataArray)
        // ------------------------------

        private DataArray deepCopyArray(DataArray original) {
            DataArray copy = DataArray.empty();
            for (int i = 0; i < original.length(); i++) {
                copy.add(deepCopyNode(original.get(i)));
            }
            return copy;
        }

        private Object deepCopyNode(Object value) {
            if (value instanceof DataObject object) {
                DataObject copy = DataObject.empty();
                for (String key : object.keys()) {
                    copy.put(key, deepCopyNode(object.get(key)));
                }
                return copy;
            }

            if (value instanceof DataArray array) {
                return deepCopyArray(array);
            }

            // String/Number/Boolean/null
            return value;
        }
    }

    // ======================================================================
    // Exemplo de uso
    // ======================================================================

    public static void exampleUsage() {
        String jsonTemplate = """
                [
                  {
                    "type": 1,
                    "components": [
                      {"type": 2, "style": 1, "custom_id": "button", "label": "Click me, {user}!"}
                    ]
                  },
                  {
                    "type": 9,
                    "components": [
                      {"type": 10, "content": "Olá {user}, bem-vindo!"}
                    ],
                    "accessory": {
                      "type": 11,
                      "media": {"url": "attachment://logo.png"},
                      "spoiler": false
                    }
                  }
                ]
                """;

        PlaceholderEngine placeholderEngine = new CurlyBracesPlaceholderEngine();
        TextFieldPolicy textFieldPolicy = new DefaultTextFieldPolicy();
        JdaMessageTemplateProcessor processor = new JdaMessageTemplateProcessor(placeholderEngine, textFieldPolicy);

        // Se você for enviar e usar "attachment://logo.png", você precisa anexar um FileUpload com nome "logo.png".
        // Se for só “montar classes”, pode deixar vazio.
        List<FileUpload> implicitUploads = List.of();

        TemplateMessageComponents template = processor.deserializeTemplate(jsonTemplate, implicitUploads);

        Map<String, String> values = Map.of("user", "Jhonatas");

        RenderedMessageComponents rendered = processor.renderTemplate(template, values);

        System.out.println("Placeholders no template: " + rendered.placeholderReport().placeholdersFoundInTemplate());
        System.out.println("Missing: " + rendered.placeholderReport().placeholdersMissingValues());
        System.out.println("Unused: " + rendered.placeholderReport().placeholdersUnusedValues());

        String finalJson = processor.serializeToJson(rendered.components());
        System.out.println(finalJson);
    }
}
Editor is loading...
Leave a Comment