├── .gitignore ├── media └── Logo-Alpha.png ├── src └── main │ ├── java │ └── de │ │ └── ialistannen │ │ └── doctor │ │ ├── DocTorConfig.java │ │ ├── util │ │ ├── Result.java │ │ ├── ArgumentParser.java │ │ ├── ParseError.java │ │ ├── ArgumentParsers.java │ │ └── StringReader.java │ │ ├── DocTor.java │ │ ├── command │ │ ├── UpdateSlashesCommand.java │ │ ├── MessageCommand.java │ │ ├── CommandListener.java │ │ └── DocCommand.java │ │ ├── storage │ │ ├── MultiFileStorage.java │ │ └── ActiveMessages.java │ │ └── rendering │ │ ├── TooManyEmbedBuilder.java │ │ ├── FormatUtils.java │ │ ├── DeclarationFormatter.java │ │ └── DocEmbedBuilder.java │ └── resources │ └── config.toml ├── README.md ├── LICENSE ├── .github └── workflows │ └── build.yaml └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /target/ 3 | /dependency-reduced-pom.xml 4 | -------------------------------------------------------------------------------- /media/Logo-Alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/I-Al-Istannen/Doctor/HEAD/media/Logo-Alpha.png -------------------------------------------------------------------------------- /src/main/java/de/ialistannen/doctor/DocTorConfig.java: -------------------------------------------------------------------------------- 1 | package de.ialistannen.doctor; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import java.util.List; 5 | 6 | public record DocTorConfig( 7 | @JsonProperty("token") String token, 8 | @JsonProperty("author_id") String authorId, 9 | @JsonProperty("sources") List sources 10 | ) { 11 | 12 | public record SourceConfig( 13 | @JsonProperty("database") String database, 14 | @JsonProperty("external_javadoc") List externalJavadoc, 15 | @JsonProperty("javadoc_url") String javadocUrl 16 | ) { 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/de/ialistannen/doctor/util/Result.java: -------------------------------------------------------------------------------- 1 | package de.ialistannen.doctor.util; 2 | 3 | import java.util.Optional; 4 | 5 | public class Result { 6 | 7 | private final T value; 8 | private final E error; 9 | 10 | private Result(T value, E error) { 11 | this.value = value; 12 | this.error = error; 13 | } 14 | 15 | public Optional getValue() { 16 | return Optional.ofNullable(value); 17 | } 18 | 19 | public Optional getError() { 20 | return Optional.ofNullable(error); 21 | } 22 | 23 | public T getOrThrow() throws E { 24 | if (value != null) { 25 | return value; 26 | } 27 | throw error; 28 | } 29 | 30 | public static Result ok(T val) { 31 | return new Result<>(val, null); 32 | } 33 | 34 | public static Result error(E error) { 35 | return new Result<>(null, error); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Doc-Tor

4 |
5 | 6 | Did you ever wish to tell somebody on discord to just read the (Java-)docs 7 | while not being rude? Did you ever wish to point out some cool Java method or 8 | class or highlight specific parts of the documentation? 9 | Then Doctor is for you :) 10 | 11 | Doctor is best explained by a short demo: 12 | 13 | 14 | https://user-images.githubusercontent.com/20284688/202914183-48fa576f-d6e2-4142-b5ea-5d12216f10a1.mp4 15 | 16 | 17 | ## Running Doctor 18 | The Doctor jar file can be build using `mvn package` and just takes a single 19 | argument: 20 | ``` 21 | java -jar Doctor.jar 22 | ``` 23 | 24 | Most of the magic happens in the config file. You can see an example in 25 | [`src/main/resources/config.toml`](src/main/resources/config.toml). 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 I-Al-Istannen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/de/ialistannen/doctor/DocTor.java: -------------------------------------------------------------------------------- 1 | package de.ialistannen.doctor; 2 | 3 | import com.fasterxml.jackson.dataformat.toml.TomlMapper; 4 | import de.ialistannen.doctor.command.CommandListener; 5 | import de.ialistannen.doctor.command.DocCommand; 6 | import de.ialistannen.doctor.storage.ActiveMessages; 7 | import java.io.IOException; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.sql.SQLException; 11 | import net.dv8tion.jda.api.JDA; 12 | import net.dv8tion.jda.api.JDABuilder; 13 | 14 | public class DocTor { 15 | 16 | public static void main(String[] args) throws IOException, InterruptedException, SQLException { 17 | TomlMapper mapper = TomlMapper.builder().build(); 18 | 19 | DocTorConfig config = mapper.readValue( 20 | Files.readString(Path.of(args[0])), 21 | DocTorConfig.class 22 | ); 23 | 24 | ActiveMessages activeMessages = new ActiveMessages(); 25 | DocCommand docCommand = DocCommand.create(config, activeMessages); 26 | 27 | JDA jda = JDABuilder.createDefault(config.token()) 28 | .addEventListeners(new CommandListener(config, docCommand, activeMessages)) 29 | .build() 30 | .awaitReady(); 31 | System.out.println(jda.getInviteUrl()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/de/ialistannen/doctor/command/UpdateSlashesCommand.java: -------------------------------------------------------------------------------- 1 | package de.ialistannen.doctor.command; 2 | 3 | import java.awt.Color; 4 | import java.util.List; 5 | import java.util.concurrent.ExecutionException; 6 | import net.dv8tion.jda.api.EmbedBuilder; 7 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 8 | import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; 9 | 10 | public class UpdateSlashesCommand { 11 | 12 | private final String authorId; 13 | 14 | public UpdateSlashesCommand(String authorId) { 15 | this.authorId = authorId; 16 | } 17 | 18 | public void run(MessageReceivedEvent event) throws ExecutionException, InterruptedException { 19 | if (!event.getAuthor().getId().equals(authorId)) { 20 | return; 21 | } 22 | 23 | event.getGuild().updateCommands() 24 | .addCommands(List.of(DocCommand.COMMAND)) 25 | .submit() 26 | .get(); 27 | 28 | event.getMessage().reply( 29 | new MessageCreateBuilder() 30 | .setEmbeds( 31 | new EmbedBuilder() 32 | .setTitle("Commands reloaded") 33 | .setColor(Color.GREEN) 34 | .setFooter("This has been widely regarded as a bad move") 35 | .build() 36 | ) 37 | .build() 38 | ).queue(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/de/ialistannen/doctor/util/ArgumentParser.java: -------------------------------------------------------------------------------- 1 | package de.ialistannen.doctor.util; 2 | 3 | import de.ialistannen.doctor.util.Result; 4 | 5 | public interface ArgumentParser { 6 | 7 | /** 8 | * Tries to parse an Argument. 9 | * 10 | * @param reader the reader to parse from 11 | * @return the parsed value or an error 12 | */ 13 | Result parse(StringReader reader); 14 | 15 | default ArgumentParser or(ArgumentParser other) { 16 | return reader -> { 17 | int start = reader.getPosition(); 18 | Result myResult = parse(reader); 19 | if (myResult.getValue().isPresent()) { 20 | return myResult; 21 | } 22 | reader.reset(start); 23 | 24 | return other.parse(reader); 25 | }; 26 | } 27 | 28 | default ArgumentParser andThen(ArgumentParser next) { 29 | return reader -> { 30 | Result myResult = parse(reader); 31 | if (myResult.getValue().isEmpty()) { 32 | @SuppressWarnings("unchecked") 33 | Result r = (Result) myResult; 34 | return r; 35 | } 36 | return next.parse(reader); 37 | }; 38 | } 39 | 40 | default boolean canParse(StringReader reader) { 41 | int start = reader.getPosition(); 42 | 43 | if (parse(reader).getValue().isPresent()) { 44 | return true; 45 | } 46 | 47 | reader.reset(start); 48 | return false; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/de/ialistannen/doctor/util/ParseError.java: -------------------------------------------------------------------------------- 1 | package de.ialistannen.doctor.util; 2 | 3 | public class ParseError extends RuntimeException { 4 | 5 | private final String input; 6 | private final int position; 7 | private final String message; 8 | 9 | public ParseError(String message, StringReader reader) { 10 | super(message); 11 | this.message = message; 12 | this.input = reader.getUnderlying(); 13 | this.position = reader.getPosition(); 14 | } 15 | 16 | @Override 17 | public String toString() { 18 | String lineWithError = input; 19 | int currentOffset = 0; 20 | int positionInErrorLine = 0; 21 | for (String line : input.lines().toList()) { 22 | if (position >= currentOffset && position <= line.length() + currentOffset) { 23 | lineWithError = line; 24 | positionInErrorLine = position - currentOffset; 25 | break; 26 | } 27 | } 28 | 29 | if (lineWithError.length() > 120) { 30 | int start = Math.max(0, positionInErrorLine - 120 / 2); 31 | int end = Math.min(lineWithError.length(), positionInErrorLine + 120 / 2); 32 | lineWithError = lineWithError.substring(start, end); 33 | positionInErrorLine = positionInErrorLine - start; 34 | } 35 | 36 | String pointerLine = " ".repeat(positionInErrorLine) + "^"; 37 | String errorPadding = " ".repeat(Math.max(0, positionInErrorLine - message.length() / 2)); 38 | String centeredError = errorPadding + message; 39 | 40 | return lineWithError + "\n" + pointerLine + "\n" + centeredError; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/de/ialistannen/doctor/storage/MultiFileStorage.java: -------------------------------------------------------------------------------- 1 | package de.ialistannen.doctor.storage; 2 | 3 | import de.ialistannen.doctor.DocTorConfig.SourceConfig; 4 | import de.ialistannen.javadocbpi.model.elements.DocumentedElement; 5 | import de.ialistannen.javadocbpi.model.elements.DocumentedElementReference; 6 | import de.ialistannen.javadocbpi.storage.SQLiteStorage; 7 | import java.io.IOException; 8 | import java.sql.SQLException; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | 12 | public class MultiFileStorage { 13 | 14 | private final Map databases; 15 | 16 | public MultiFileStorage(Map databases) { 17 | this.databases = databases; 18 | } 19 | 20 | public Optional get(String qualifiedName) throws SQLException, IOException { 21 | for (var database : databases.entrySet()) { 22 | Optional element = database.getValue().get(qualifiedName); 23 | if (element.isPresent()) { 24 | return element.map(it -> FetchResult.fromUnderlying(it, database.getKey())); 25 | } 26 | } 27 | return Optional.empty(); 28 | } 29 | 30 | public record FetchResult( 31 | DocumentedElementReference reference, 32 | DocumentedElement element, 33 | SourceConfig config 34 | ) { 35 | 36 | private static FetchResult fromUnderlying( 37 | SQLiteStorage.FetchResult result, 38 | SourceConfig config 39 | ) { 40 | return new FetchResult(result.reference(), result.element(), config); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/resources/config.toml: -------------------------------------------------------------------------------- 1 | token = "token" 2 | author_id = "id" 3 | 4 | [[sources]] 5 | external_javadoc = [] 6 | database = "/home/i_al_istannen/src/spoon/JavadocIndexer/target/Jdk-Index.db" 7 | javadoc_url = "https://docs.oracle.com/en/java/javase/22/docs/api/" 8 | 9 | [[sources]] 10 | external_javadoc = [ 11 | "https://guava.dev/releases/21.0/api/docs/", 12 | "https://javadoc.io/doc/org.yaml/snakeyaml/1.28/", 13 | "https://javadoc.io/doc/org.jetbrains/annotations/21.0.1/", 14 | "https://javadoc.io/doc/net.md-5/bungeecord-chat/1.16-R0.4/", 15 | "https://jd.adventure.kyori.net/api/4.17.0/", 16 | "https://jd.adventure.kyori.net/text-serializer-gson/4.8.1/", 17 | "https://jd.adventure.kyori.net/text-serializer-legacy/4.8.1/", 18 | "https://jd.adventure.kyori.net/text-serializer-plain/4.8.1/", 19 | "https://docs.oracle.com/en/java/javase/22/docs/api" 20 | ] 21 | database = "/home/i_al_istannen/src/spoon/JavadocIndexer/target/Paper-Api-Index.db" 22 | javadoc_url = "https://jd.papermc.io/paper/1.21/" 23 | 24 | [[sources]] 25 | external_javadoc = [ 26 | "https://docs.oracle.com/en/java/javase/22/docs/api" 27 | ] 28 | database = "/home/i_al_istannen/src/spoon/JavadocIndexer/target/JDA-Index.db" 29 | javadoc_url = "https://ci.dv8tion.net/job/JDA/javadoc/" 30 | 31 | [[sources]] 32 | external_javadoc = [ 33 | "https://docs.oracle.com/en/java/javase/22/docs/api" 34 | ] 35 | database = "/home/i_al_istannen/src/spoon/JavadocIndexer/target/JavaFx-Index.db" 36 | javadoc_url = "https://openjfx.io/javadoc/22/" 37 | 38 | [[sources]] 39 | external_javadoc = [ 40 | "https://docs.oracle.com/en/java/javase/22/docs/api" 41 | ] 42 | database = "/home/i_al_istannen/src/spoon/JavadocIndexer/target/ApFloat-Index.db" 43 | javadoc_url = "http://www.apfloat.org/apfloat_java/docs/" 44 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - 'master' 8 | tags: 9 | - 'v*' 10 | pull_request: 11 | branches: 12 | - 'master' 13 | 14 | env: 15 | IMAGE_NAME: "ghcr.io/i-al-istannen/doctor" 16 | 17 | jobs: 18 | docker: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - uses: actions/setup-java@v4 25 | with: 26 | distribution: 'temurin' 27 | java-version: '22' 28 | 29 | - name: Docker meta 30 | id: meta 31 | uses: docker/metadata-action@v5 32 | with: 33 | images: "${{ env.IMAGE_NAME }}" 34 | sep-tags: "," 35 | sep-labels: "," 36 | tags: | 37 | type=ref,event=branch 38 | type=ref,event=pr 39 | type=ref,event=tag 40 | type=semver,pattern={{version}} 41 | type=semver,pattern={{major}}.{{minor}} 42 | type=edge,branch=master 43 | type=sha 44 | type=sha,format=long 45 | 46 | - name: Login to registry 47 | uses: docker/login-action@v3 48 | with: 49 | registry: ghcr.io 50 | username: "${{ github.repository_owner }}" 51 | password: "${{ secrets.GITHUB_TOKEN }}" 52 | 53 | - name: Remove prefix from tags (stolen) 54 | run: echo "fixed_tags=${{ steps.meta.outputs.tags }}" | sed 's;${{ env.IMAGE_NAME }}:;;g' >> $GITHUB_ENV 55 | 56 | - name: Build and publish docker image 57 | run: | 58 | mvn package jib:build \ 59 | -Djib.to.tags="${{ env.fixed_tags }}" \ 60 | -Djib.container.labels="${{ steps.meta.outputs.labels }}" \ 61 | -Djib.console=plain \ 62 | -Djib.to.image="${{ env.IMAGE_NAME }}" 63 | -------------------------------------------------------------------------------- /src/main/java/de/ialistannen/doctor/command/MessageCommand.java: -------------------------------------------------------------------------------- 1 | package de.ialistannen.doctor.command; 2 | 3 | import de.ialistannen.doctor.rendering.DocEmbedBuilder.DescriptionStyle; 4 | import de.ialistannen.doctor.storage.ActiveMessages.ActiveMessage; 5 | import java.util.Arrays; 6 | import java.util.Optional; 7 | import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; 8 | 9 | public enum MessageCommand { 10 | COLLAPSE("⏫", "Collapse", "collapse", ButtonStyle.SECONDARY), 11 | EXPAND("⏬", "Expand", "expand", ButtonStyle.SECONDARY), 12 | REMOVE_TAGS("✂️", "Hide Tags", "remove_tags", ButtonStyle.SECONDARY), 13 | ADD_TAGS("📝", "Show Tags", "add_tags", ButtonStyle.SECONDARY), 14 | DELETE("🗑️", "Delete", "delete", ButtonStyle.DANGER); 15 | 16 | private final ButtonStyle buttonStyle; 17 | private final String icon; 18 | private final String label; 19 | private final String id; 20 | 21 | MessageCommand(String icon, String label, String id, ButtonStyle buttonStyle) { 22 | this.buttonStyle = buttonStyle; 23 | this.icon = icon; 24 | this.label = label; 25 | this.id = id; 26 | } 27 | 28 | public String getIcon() { 29 | return icon; 30 | } 31 | 32 | public String getLabel() { 33 | return label; 34 | } 35 | 36 | public String getId() { 37 | return id; 38 | } 39 | 40 | public ButtonStyle getButtonStyle() { 41 | return buttonStyle; 42 | } 43 | 44 | public Optional update(ActiveMessage message) { 45 | return switch (this) { 46 | case COLLAPSE -> Optional.of(message.withDescriptionStyle(DescriptionStyle.SHORT)); 47 | case EXPAND -> Optional.of(message.withDescriptionStyle(DescriptionStyle.LONG)); 48 | case REMOVE_TAGS -> Optional.of(message.withTags(false)); 49 | case ADD_TAGS -> Optional.of(message.withTags(true)); 50 | case DELETE -> Optional.empty(); 51 | }; 52 | } 53 | 54 | public static Optional fromId(String id) { 55 | return Arrays.stream(values()) 56 | .filter(it -> it.getId().equals(id)) 57 | .findFirst(); 58 | } 59 | 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/main/java/de/ialistannen/doctor/storage/ActiveMessages.java: -------------------------------------------------------------------------------- 1 | package de.ialistannen.doctor.storage; 2 | 3 | import com.github.benmanes.caffeine.cache.Cache; 4 | import com.github.benmanes.caffeine.cache.Caffeine; 5 | import de.ialistannen.doctor.rendering.DocEmbedBuilder.DescriptionStyle; 6 | import java.util.Optional; 7 | import java.util.UUID; 8 | 9 | public class ActiveMessages { 10 | 11 | private final Cache activeChoosers; 12 | private final Cache activeMessages; 13 | 14 | public ActiveMessages() { 15 | this.activeChoosers = Caffeine.newBuilder() 16 | .maximumSize(1000) 17 | .build(); 18 | this.activeMessages = Caffeine.newBuilder() 19 | .maximumSize(1000) 20 | .build(); 21 | } 22 | 23 | public String registerChooser(ActiveChooser chooser) { 24 | String id = UUID.randomUUID().toString(); 25 | activeChoosers.put(id, chooser); 26 | return id; 27 | } 28 | 29 | public Optional lookupChooser(String id) { 30 | return Optional.ofNullable(activeChoosers.getIfPresent(id)); 31 | } 32 | 33 | public ActiveMessage registerMessage(String messageId, ActiveMessage message) { 34 | activeMessages.put(messageId, message); 35 | return message; 36 | } 37 | 38 | public Optional lookupMessage(String messageId) { 39 | return Optional.ofNullable(activeMessages.getIfPresent(messageId)); 40 | } 41 | 42 | public void deleteMessage(String messageId) { 43 | activeMessages.invalidate(messageId); 44 | } 45 | 46 | public record ActiveChooser( 47 | String ownerId, 48 | String qualifiedName 49 | ) { 50 | 51 | } 52 | 53 | public record ActiveMessage( 54 | String ownerId, 55 | String qualifiedName, 56 | DescriptionStyle descriptionStyle, 57 | boolean tags, 58 | boolean expandable 59 | ) { 60 | 61 | public ActiveMessage withDescriptionStyle(DescriptionStyle style) { 62 | return new ActiveMessage(ownerId(), qualifiedName(), style, tags(), expandable); 63 | } 64 | 65 | public ActiveMessage withTags(boolean tags) { 66 | return new ActiveMessage(ownerId(), qualifiedName(), descriptionStyle(), tags, expandable); 67 | } 68 | 69 | public ActiveMessage withExpandable(boolean expandable) { 70 | return new ActiveMessage(ownerId(), qualifiedName(), descriptionStyle(), tags, expandable); 71 | } 72 | 73 | public static ActiveMessage of(String ownerId, String qualifiedName) { 74 | return new ActiveMessage(ownerId, qualifiedName, DescriptionStyle.SHORT, false, true); 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/de/ialistannen/doctor/rendering/TooManyEmbedBuilder.java: -------------------------------------------------------------------------------- 1 | package de.ialistannen.doctor.rendering; 2 | 3 | import de.ialistannen.doctor.storage.ActiveMessages; 4 | import de.ialistannen.doctor.storage.ActiveMessages.ActiveChooser; 5 | import de.ialistannen.doctor.storage.MultiFileStorage; 6 | import de.ialistannen.doctor.storage.MultiFileStorage.FetchResult; 7 | import de.ialistannen.javadocbpi.util.NameShortener; 8 | import java.io.IOException; 9 | import java.sql.SQLException; 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Optional; 15 | import java.util.Set; 16 | import java.util.stream.Collectors; 17 | import net.dv8tion.jda.api.interactions.components.ActionRow; 18 | import net.dv8tion.jda.api.interactions.components.Component.Type; 19 | import net.dv8tion.jda.api.interactions.components.ItemComponent; 20 | import net.dv8tion.jda.api.interactions.components.buttons.Button; 21 | import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; 22 | import net.dv8tion.jda.api.utils.messages.AbstractMessageBuilder; 23 | 24 | public class TooManyEmbedBuilder { 25 | 26 | private final static int MAX_ROWS = 5; 27 | private final MultiFileStorage storage; 28 | private final ActiveMessages activeMessages; 29 | 30 | public TooManyEmbedBuilder(MultiFileStorage storage, ActiveMessages activeMessages) { 31 | this.storage = storage; 32 | this.activeMessages = activeMessages; 33 | } 34 | 35 | public void buildMessage( 36 | String ownerId, 37 | Collection qualifiedNames, 38 | AbstractMessageBuilder builder 39 | ) throws SQLException, IOException { 40 | Set potentialMatches = qualifiedNames.stream() 41 | .distinct() 42 | .limit((long) Type.BUTTON.getMaxPerRow() * MAX_ROWS) 43 | .collect(Collectors.toSet()); 44 | 45 | Map qualifiedNameLabelMap = new NameShortener() 46 | .shortenMatches(potentialMatches); 47 | 48 | List rows = new ArrayList<>(); 49 | for (var chunk : chunk(qualifiedNameLabelMap.entrySet(), MAX_ROWS)) { 50 | List components = new ArrayList<>(); 51 | for (var entry : chunk) { 52 | buildButton(ownerId, entry.getKey(), entry.getValue()) 53 | .ifPresent(components::add); 54 | } 55 | if (!components.isEmpty()) { 56 | rows.add(ActionRow.of(components)); 57 | } 58 | } 59 | builder.setComponents(rows); 60 | } 61 | 62 | private Optional