Skip to content

Commit

Permalink
Add Haiku Analyze message command (#1037)
Browse files Browse the repository at this point in the history
* Add Haiku Analyze message command

* Fix tests

* Fix tests

* Add special case for Ninbot Haiku messages

* Add share button

* Add docs

* Add tests
  • Loading branch information
Nincodedo authored Aug 24, 2024
1 parent c38fb0f commit e377cd6
Show file tree
Hide file tree
Showing 14 changed files with 452 additions and 206 deletions.
4 changes: 4 additions & 0 deletions docs/changelog/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Detailed changelogs can be found on the [Ninbot GitHub Releases page](https://github.com/Nincodedo/Ninbot/releases).
This page will highlight more interesting changes between versions.

## [3.19.0](https://github.com/Nincodedo/Ninbot/releases/tag/3.19.0)

* Added the [Haiku Analyzer](../commands/index.md#haiku-analyzer) message command

## [3.9.0](https://github.com/Nincodedo/Ninbot/releases/tag/3.9.0)

* Added the [Emojitizer](../commands/index.md#emojitizer) message command
Expand Down
5 changes: 5 additions & 0 deletions docs/commands/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,8 @@ Like the Dab slash command, dabs on that specific message. There is also a Huge
Emojitizer lets you enter a word you want to turn into reaction emojis on a message. You can only use each letter once
so words with duplicate letters are not allowed. Also, if you use Emojitizer on a message that already has emoji letters
on it, those are not available to you.

### Haiku Analyzer

The Haiku Analyzer breaks down why a message is or isn't considered a haiku. On first run it will only show the results
to you. You can click the share button on the results to send it as a message to the rest of the channel.
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package dev.nincodedo.ninbot.components.haiku;

import dev.nincodedo.nincord.Emojis;
import dev.nincodedo.nincord.command.message.MessageContextCommand;
import dev.nincodedo.nincord.message.MessageContextInteractionEventMessageExecutor;
import dev.nincodedo.nincord.message.MessageExecutor;
import dev.nincodedo.nincord.message.MessageUtils;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import net.dv8tion.jda.api.utils.MarkdownSanitizer;
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
public class HaikuAnalyzeCommand implements MessageContextCommand {

private HaikuMessageParser haikuMessageParser;

public HaikuAnalyzeCommand(HaikuMessageParser haikuMessageParser) {
this.haikuMessageParser = haikuMessageParser;
}

@Override
public MessageExecutor execute(@NotNull MessageContextInteractionEvent event, @NotNull MessageContextInteractionEventMessageExecutor messageExecutor) {
var message = getContentStrippedMessage(event);
var rawMessage = getRawMessage(event);
boolean messageHasCharacters = !message.isEmpty();
boolean messageOnlyCharacters = haikuMessageParser.isMessageOnlyCharacters(message);
boolean messageIsCorrectSyllables = haikuMessageParser.getSyllableCount(message) == 17;
int calculatedSyllableTotal = haikuMessageParser.getSyllableCount(message);

boolean correctNumberOfLines = false;
boolean line1SyllablesCorrect = false;
boolean line2SyllablesCorrect = false;
boolean line3SyllablesCorrect = false;

EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.setTitle("Message Haikuability Analysis");
embedBuilder.setUrl(event.getTarget().getJumpUrl());
embedBuilder.addField("Has text", Emojis.getCheckOrXResponse(messageHasCharacters), true);
if (messageHasCharacters) {
embedBuilder.addField("Only characters", Emojis.getCheckOrXResponse(messageOnlyCharacters), true);
}
if (messageOnlyCharacters) {
embedBuilder.addField("17 syllables", Emojis.getCheckOrXResponse(messageIsCorrectSyllables), true);
}
List<Integer> lineTotals = new ArrayList<>();
if (messageOnlyCharacters && messageIsCorrectSyllables) {
var splitMessage = message.split("\\s+");
StringBuilder lines = new StringBuilder();
int syllableTotal = 0;
int lineSyllableTotal = 0;
int nextSyllableCount = 5;
for (int i = 0; i < splitMessage.length; i++) {
var word = splitMessage[i];
var wordSyllable = haikuMessageParser.getSyllableCount(word);
syllableTotal += wordSyllable;
lineSyllableTotal += wordSyllable;
lines.append(word).append(" (").append(wordSyllable).append(") ");
if (lineSyllableTotal >= nextSyllableCount) {
lines.append(String.format(" = %s actual, %s expected", lineSyllableTotal, nextSyllableCount));
lines.append("\n");
if (nextSyllableCount == 7) {
nextSyllableCount = 5;
} else {
nextSyllableCount = 7;
}
lineTotals.add(lineSyllableTotal);
lineSyllableTotal = 0;
}
if ((syllableTotal >= 17 || lineTotals.size() == 3 || i == splitMessage.length - 1) && lineSyllableTotal > 0) {
lines.append(String.format(" = %s actual, %s expected", lineSyllableTotal, nextSyllableCount));
lineTotals.add(lineSyllableTotal);
break;
}
}
correctNumberOfLines = lineTotals.size() == 3;
line1SyllablesCorrect = !lineTotals.isEmpty() && lineTotals.get(0) == 5;
line2SyllablesCorrect = lineTotals.size() >= 2 && lineTotals.get(1) == 7;
line3SyllablesCorrect = lineTotals.size() == 3 && lineTotals.get(2) == 5;
embedBuilder.addField("Line Analysis", MessageUtils.addSpoilerText(lines.toString(), rawMessage), false);
} else if (messageOnlyCharacters) {
var splitMessage = message.split("\\s+");
StringBuilder messageAnalysis = new StringBuilder();
for (String word : splitMessage) {
var wordSyllable = haikuMessageParser.getSyllableCount(word);
messageAnalysis.append(word).append(" (").append(wordSyllable).append(") ");
}
embedBuilder.addField("Message Analysis", MessageUtils.addSpoilerText(messageAnalysis.toString(), rawMessage), false);
}

String overallResponse = overallResponseMessage(messageOnlyCharacters, messageIsCorrectSyllables, correctNumberOfLines, line1SyllablesCorrect, line2SyllablesCorrect, line3SyllablesCorrect, messageHasCharacters, calculatedSyllableTotal, lineTotals);

embedBuilder.addField("Overall", overallResponse, false);

MessageCreateBuilder createBuilder = new MessageCreateBuilder();
createBuilder.addEmbeds(embedBuilder.build());
createBuilder.addActionRow(Button.primary("haiku-share", "Share to channel"));

messageExecutor.addEphemeralMessage(createBuilder.build());
return messageExecutor;
}

private @NotNull String overallResponseMessage(boolean messageOnlyCharacters, boolean messageIsCorrectSyllables, boolean correctNumberOfLines, boolean line1SyllablesCorrect, boolean line2SyllablesCorrect, boolean line3SyllablesCorrect, boolean messageHasCharacters, int calculatedSyllableTotal, List<Integer> lineTotals) {
String overallResponse;
if (messageOnlyCharacters && messageIsCorrectSyllables && correctNumberOfLines && line1SyllablesCorrect && line2SyllablesCorrect && line3SyllablesCorrect) {
overallResponse = "Haikuable";
} else {
overallResponse = "Not Haikuable";
String additionalReason = ". Too %s %s.";
if (!messageHasCharacters) {
overallResponse += ". Message has no text.";
} else if (!messageOnlyCharacters) {
overallResponse += ". Message has unsyllable characters.";
} else if (calculatedSyllableTotal != 17) {
overallResponse += String.format(additionalReason, getFewOrMany(calculatedSyllableTotal < 17), String.format("syllables: %s", calculatedSyllableTotal));
} else if (lineTotals.size() != 3) {
overallResponse += String.format(additionalReason, getFewOrMany(lineTotals.size() < 3), "lines");
} else if (lineTotals.get(0) != 5) {
overallResponse += String.format(additionalReason, getFewOrMany(lineTotals.getFirst() < 5), " syllables in line 1");
} else if (lineTotals.get(1) != 7) {
overallResponse += String.format(additionalReason, getFewOrMany(lineTotals.get(1) < 7), "syllables in line 2");
} else if (lineTotals.get(2) != 5) {
overallResponse += String.format(additionalReason, getFewOrMany(lineTotals.get(2) < 5), "syllables in line 3");
} else {
overallResponse += ". Heck I dunno how you got here.";
}
}
return overallResponse;
}

private @NotNull String getContentStrippedMessage(@NotNull MessageContextInteractionEvent event) {
var message = event.getTarget();
if (message.getAuthor().equals(event.getJDA().getSelfUser())) {
var embeds = event.getTarget().getEmbeds();
return embeds.getFirst().getDescription() == null ? "" : MarkdownSanitizer.sanitize(embeds.getFirst().getDescription());
} else {
return event.getTarget().getContentStripped();
}
}

private @NotNull String getRawMessage(@NotNull MessageContextInteractionEvent event) {
var message = event.getTarget();
if (message.getAuthor().equals(event.getJDA().getSelfUser())) {
var embeds = event.getTarget().getEmbeds();
return embeds.getFirst().getDescription() == null ? "" : embeds.getFirst().getDescription();
} else {
return event.getTarget().getContentRaw();
}
}

private @NotNull String getFewOrMany(boolean isLessThan) {
return isLessThan ? "few" : "many";
}

@Override
public String getName() {
return HaikuCommandName.HAIKU.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dev.nincodedo.ninbot.components.haiku;

import dev.nincodedo.nincord.command.component.ButtonInteraction;
import dev.nincodedo.nincord.command.component.ComponentData;
import dev.nincodedo.nincord.message.ButtonInteractionCommandMessageExecutor;
import dev.nincodedo.nincord.message.MessageExecutor;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class HaikuAnalyzeShareButtonInteraction implements ButtonInteraction {
@Override
public MessageExecutor execute(@NotNull ButtonInteractionEvent event,
@NotNull ButtonInteractionCommandMessageExecutor messageExecutor, @NotNull ComponentData componentData) {
MessageCreateBuilder messageCreateBuilder = new MessageCreateBuilder();
messageCreateBuilder.setEmbeds(event.getMessage().getEmbeds());
messageExecutor.addMessageResponse(messageCreateBuilder.build());
return messageExecutor;
}

@Override
public Logger log() {
return log;
}

@Override
public String getName() {
return "haiku";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.nincodedo.ninbot.components.haiku;

import dev.nincodedo.nincord.command.CommandNameEnum;

enum HaikuCommandName implements CommandNameEnum {
HAIKU
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import dev.nincodedo.nincord.config.db.component.ComponentType;
import dev.nincodedo.nincord.message.MessageUtils;
import dev.nincodedo.nincord.stats.StatManager;
import eu.crydee.syllablecounter.SyllableCounter;
import io.micrometer.core.instrument.Metrics;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import net.dv8tion.jda.api.EmbedBuilder;
Expand All @@ -17,28 +16,24 @@
import org.springframework.stereotype.Component;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.regex.Pattern;

@Component
public class HaikuListener extends StatAwareListenerAdapter {

private ComponentService componentService;
private ConfigService configService;
private SyllableCounter syllableCounter;
private HaikuMessageParser haikuMessageParser;
private Random random;
private String componentName;

public HaikuListener(StatManager statManager, @Qualifier("statCounterThreadPool") ExecutorService executorService
, ComponentService componentService, ConfigService configService) {
, ComponentService componentService, ConfigService configService, HaikuMessageParser haikuMessageParser) {
super(statManager, executorService);
this.componentService = componentService;
this.configService = configService;
this.syllableCounter = new SyllableCounter();
this.haikuMessageParser = haikuMessageParser;
this.random = new SecureRandom();
this.componentName = "haiku";
componentService.registerComponent(componentName, ComponentType.ACTION);
Expand All @@ -53,7 +48,11 @@ public void onMessageReceived(MessageReceivedEvent event) {
}
var message = event.getMessage().getContentStripped();
var guildId = event.getGuild().getId();
isHaikuable(message, guildId).ifPresent(haikuLines -> {
Metrics.counter("bot.listener.haiku.checked").increment();
haikuMessageParser.isHaikuable(message).ifPresent(haikuLines -> {
if (!checkChance(guildId)) {
return;
}
EmbedBuilder embedBuilder = new EmbedBuilder();
embedBuilder.appendDescription(MessageUtils.addSpoilerText(
"_" + haikuLines + "_", event.getMessage().getContentRaw()));
Expand All @@ -65,57 +64,6 @@ public void onMessageReceived(MessageReceivedEvent event) {
});
}

Optional<String> isHaikuable(String message, String guildId) {
Metrics.counter("bot.listener.haiku.checked").increment();
if (isMessageOnlyCharacters(message) && getSyllableCount(message) == 17) {
String[] splitMessage = message.split("\\s+");
List<String> lines = new ArrayList<>();
var results = checkLine(0, 5, splitMessage);
if (results.counter() == -1) {
return Optional.empty();
} else {
lines.add(results.line());
}
results = checkLine(results.counter(), 7, splitMessage);
if (results.counter() == -1) {
return Optional.empty();
} else {
lines.add(results.line());
}
results = checkLine(results.counter(), 5, splitMessage);
if (results.counter() == -1) {
return Optional.empty();
} else {
lines.add(results.line());
}
if (results.counter() == splitMessage.length && checkChance(guildId)) {
return Optional.of(lines.get(0) + "\n" + lines.get(1) + "\n" + lines.get(2));
}
}
return Optional.empty();
}

private HaikuParsingResults checkLine(int counter, int syllables, String[] splitMessage) {
StringBuilder line = new StringBuilder();
for (; counter < splitMessage.length; counter++) {
String word = splitMessage[counter];
line.append(word);
line.append(" ");
int syllableCount = getSyllableCount(line.toString());
if (syllableCount > syllables) {
return new HaikuParsingResults(-1, line.toString());
} else if (syllableCount == syllables) {
counter++;
return new HaikuParsingResults(counter, line.toString());
}
}
return new HaikuParsingResults(-1, line.toString());
}

private boolean isMessageOnlyCharacters(String message) {
return Pattern.compile("^[a-zA-Z\\sé.,!?]+$").matcher(message).matches();
}

boolean checkChance(String guildId) {
var optionalConfig = configService.getGlobalConfigByName(ConfigConstants.HAIKU_CHANCE, guildId);
int chance = 10;
Expand All @@ -124,15 +72,4 @@ boolean checkChance(String guildId) {
}
return random.nextInt(100) < chance;
}

private int getSyllableCount(String message) {
int count = 0;
for (String word : message.split("\\s+")) {
count += syllableCounter.count(word.replaceAll("\\W", ""));
}
return count;
}
}

record HaikuParsingResults(int counter, String line) {
}
Loading

0 comments on commit e377cd6

Please sign in to comment.