Skip to content

Commit

Permalink
Merge pull request #13 from Killjoyer/refactor
Browse files Browse the repository at this point in the history
Refactor
pamparamm authored Oct 18, 2020

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 8babe19 + 29f92df commit 225f46a
Showing 26 changed files with 479 additions and 171 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -11,6 +11,10 @@
# Задачи

- [ ] Билды героев Heroes of the storm в дискорде
- [ ] Базовое взаимодействие с дискордом
- [x] Базовое взаимодействие с дискордом
- [x] Получение сообщений от дискорда
- [x] Хранение состояний для связки канал+пользователь
- [x] Обработка "echo", "authors"
- [x] Обработка "build"
- [ ] Загрузка талантов героев с icyVeins
- [ ] Толерантность к ошибкам
- [ ] Толерантность к ошибкам
2 changes: 1 addition & 1 deletion checkstyle.xml
Original file line number Diff line number Diff line change
@@ -220,7 +220,7 @@
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false" />
<property name="allowedAbbreviationLength" value="0" />
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,
<property name="tokens" value="CLASS_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,
PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF,
RECORD_COMPONENT_DEF" />
</module>
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@

<groupId>org.tbplusc</groupId>
<artifactId>boostio-bot</artifactId>
<version>0.0.1</version>
<version>0.1.0</version>

<build>
<plugins>
47 changes: 43 additions & 4 deletions src/main/java/org/tbplusc/app/Main.java
Original file line number Diff line number Diff line change
@@ -7,23 +7,62 @@
import org.slf4j.LoggerFactory;
import org.tbplusc.app.discordinteraction.DefaultChatState;
import org.tbplusc.app.discordinteraction.MessageHandler;
import org.tbplusc.app.discordinteraction.WrappedDiscordMessage;
import org.tbplusc.app.talenthelper.parsers.ITalentProvider;
import org.tbplusc.app.talenthelper.parsers.IcyVeinsRemoteDataProvider;
import org.tbplusc.app.talenthelper.parsers.IcyVeinsTalentProvider;
import org.tbplusc.app.util.EnvWrapper;
import org.tbplusc.app.util.JsonDeserializer;
import org.tbplusc.app.validator.Validator;

import java.io.IOException;
import java.util.Arrays;

public class Main {
private static final Logger logger = LoggerFactory.getLogger(Main.class);

public static void main(String[] args) {
logger.info("Application started");
final var token = System.getenv("DISCORD_TOKEN");
registerEnvVariables();
final var token = EnvWrapper.getValue("DISCORD_TOKEN");
final var client = DiscordClient.create(token);
DefaultChatState.registerDefaultCommands();
final var messageHandler = new MessageHandler();
final MessageHandler messageHandler;
try {
messageHandler = createMessageHandler();
} catch (IOException e) {
logger.error("Can't create message handler", e);
return;
}
logger.info("Message handler is ready");
final var gateway = client.login().block();
if (gateway == null) {
logger.error("Can't connect to discord");
return;
}
gateway.on(MessageCreateEvent.class).map(MessageCreateEvent::getMessage)
.subscribe(messageHandler::handleMessage);
.subscribe(message -> messageHandler
.handleMessage(new WrappedDiscordMessage(message)));
gateway.on(DisconnectEvent.class).blockLast();
}

private static Validator createValidator() throws IOException {
final var heroes = JsonDeserializer.deserializeHeroList(org.tbplusc.app.util.HttpGetter
.getBodyFromUrl("https://hotsapi.net/api/v1/heroes"));
return new Validator(Arrays.asList(heroes.stream().map((hero) -> hero.name)
.toArray(String[]::new)));
}

private static ITalentProvider createIcyVeinsTalentProvider() {
return new IcyVeinsTalentProvider(new IcyVeinsRemoteDataProvider());
}

private static MessageHandler createMessageHandler() throws IOException {
DefaultChatState.registerDefaultCommands(createValidator(), createIcyVeinsTalentProvider());
return new MessageHandler();
}

private static void registerEnvVariables() {
EnvWrapper.registerValue("DISCORD_TOKEN", System.getenv("DISCORD_TOKEN"));
EnvWrapper.registerValue("DISCORD_PREFIX", System.getenv("DISCORD_PREFIX"));
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.tbplusc.app.discordinteraction;

import discord4j.core.object.entity.Message;

public interface ChatState {
ChatState handleMessage(Message message);
ChatState handleMessage(WrappedMessage message);
}
Original file line number Diff line number Diff line change
@@ -1,61 +1,65 @@
package org.tbplusc.app.discordinteraction;

import discord4j.core.object.entity.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tbplusc.app.talenthelper.HeroConsts;
import org.tbplusc.app.talenthelper.icyveinsparser.IcyVeinsParser;
import org.tbplusc.app.talenthelper.parsers.ITalentProvider;
import org.tbplusc.app.util.EnvWrapper;
import org.tbplusc.app.validator.Validator;

import static org.tbplusc.app.discordinteraction.DiscordUtil.getChannelForMessage;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;


/**
* ChatState that responsible for handling commands on first step
*/
public class DefaultChatState implements ChatState {
private static final Logger logger = LoggerFactory.getLogger(DefaultChatState.class);

private final String prefix;

private static final Map<String, BiFunction<String, Message, ChatState>> commands =
private static final Map<String, BiFunction<String, WrappedMessage, ChatState>> commands =
new HashMap<>();

public static void registerCommand(String name, BiFunction<String, Message, ChatState> action) {
public static void registerCommand(String name, BiFunction<String, WrappedMessage, ChatState> action) {
commands.put(name, action);
}

public static void registerDefaultCommands() {
/**
* Register "echo", "authors", "builds" commands.
* @param validator object to find closest word for "builds" command
* @param talentProvider object to get builds for hero
*/
public static void registerDefaultCommands(Validator validator,
ITalentProvider talentProvider) {
registerCommand("echo", (args, message) -> {
final var channel = getChannelForMessage(message);
channel.createMessage(
args.equals("") ? "Не могу заэхоть пустую строку" : args
).block();
message.respond(args.equals("") ? "Не могу заэхоть пустую строку" : args);
return new DefaultChatState();
});
registerCommand("authors", (args, message) -> {
final var channel = getChannelForMessage(message);
channel.createMessage(
"Код писали: Александ Жмышенко, Олег Белахахлий и Semen Зайдельман")
.block();
message.respond("Код писали: Александ Жмышенко, Олег Белахахлий и Semen Зайдельман");
return new DefaultChatState();
});
registerCommand("build", (args, message) -> {
final var validator = new Validator();
logger.info("Typed hero name: {}", args);
final var possibleHeroNames = validator.getSomeCosestToInput(args, 10);
return new HeroSelectionState(Arrays.asList(possibleHeroNames.clone()), message);
final var possibleHeroNames = validator.getSomeClosestToInput(args, 10);
if (possibleHeroNames[0].distance == 0) {
HeroSelectionState.showHeroBuildToDiscord(message, possibleHeroNames[0].word,
talentProvider);
return new DefaultChatState();
}
return new HeroSelectionState(Arrays.asList(possibleHeroNames.clone()), message,
talentProvider);
});
}

public DefaultChatState() {
prefix = System.getenv("DISCORD_PREFIX");
prefix = EnvWrapper.getValue("DISCORD_PREFIX");
}

@Override
public ChatState handleMessage(Message message) {
@Override public ChatState handleMessage(WrappedMessage message) {
final var content = message.getContent();
if (!content.startsWith(prefix)) {
return this;
Original file line number Diff line number Diff line change
@@ -1,45 +1,61 @@
package org.tbplusc.app.discordinteraction;

import discord4j.core.object.entity.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tbplusc.app.talenthelper.HeroConsts;
import org.tbplusc.app.talenthelper.icyveinsparser.IcyVeinsParser;
import org.tbplusc.app.talenthelper.parsers.ITalentProvider;
import org.tbplusc.app.talenthelper.parsers.IcyVeinsTalentProvider;
import org.tbplusc.app.validator.WordDistancePair;

import java.io.IOException;
import java.util.List;

import static org.tbplusc.app.discordinteraction.DiscordUtil.getChannelForMessage;

/**
* ChatState to allow user to select hero from closest to his text.
*/
public class HeroSelectionState implements ChatState {
private static final Logger logger = LoggerFactory.getLogger(HeroSelectionState.class);

private final List<String> availableHeroes;
private final List<WordDistancePair> availableHeroes;

private final Message message;
private final WrappedMessage message;
private final ITalentProvider talentProvider;

public HeroSelectionState(List<String> availableHeroes, Message message) {
/**
* ChatState to allow user to select hero from closest to his text.
* @param availableHeroes heroes list provided by Validator
* @param message message with command from user
* @param talentProvider object to get talents from somewhere
*/
public HeroSelectionState(List<WordDistancePair> availableHeroes, WrappedMessage message,
ITalentProvider talentProvider) {
this.availableHeroes = availableHeroes;
this.message = message;
this.talentProvider = talentProvider;
showInitMessage();
}

public void showInitMessage() {
final var channel = getChannelForMessage(message);
private void showInitMessage() {
final var heroes = new StringBuilder();
for (var i = 0; i < availableHeroes.size(); i++) {
heroes.append(String.format("%3d. %s \n", i + 1, availableHeroes.get(i)));
heroes.append(String.format("%3d. %s \n", i + 1, availableHeroes.get(i).word));
}
channel.createMessage(String.format("Choice hero (type number): \n ```md\n%s```", heroes))
.block();
message.respond(String.format("Choose hero (type number): \n ```md\n%s```", heroes));
}

private static void showHeroBuildToDiscord(Message message, String heroName) {
logger.info("Normalized hero name: {}", heroName);
/**
* Format selected hero build for discord and send it as message response.
* @param message message to respond
* @param heroName
* @param talentProvider object to get talents from somewhere
*/
public static void showHeroBuildToDiscord(WrappedMessage message, String heroName,
ITalentProvider talentProvider) {
final var normalizedHeroName = IcyVeinsTalentProvider.normalizeHeroName(heroName);
logger.info("Normalized hero name: {}", normalizedHeroName);
try {
final var builds = IcyVeinsParser.getBuildsByHeroName(heroName);
final var channel = getChannelForMessage(message);
channel.createMessage(String.format("Selected hero is **%s**", heroName)).block();
final var builds = talentProvider.getBuilds(normalizedHeroName);
message.respond(String.format("Selected hero is **%s**", normalizedHeroName));
builds.getBuilds().stream().map((build) -> {
final var talents = new StringBuilder();
for (var i = 0; i < build.getTalents().size(); i++) {
@@ -48,20 +64,20 @@ private static void showHeroBuildToDiscord(Message message, String heroName) {
}
return String.format("**%s**: ```md\n%s```**Description:** %s", build.getName(),
talents, build.getDescription());
}).forEach(build -> channel.createMessage(build).block());
}).forEach(message::respond);
} catch (IOException e) {
logger.error("Hero was not loaded", e);
}
}

@Override public ChatState handleMessage(Message message) {
@Override public ChatState handleMessage(WrappedMessage message) {
var number = Integer.parseInt(message.getContent());
if (number <= 1 || number >= 10) {
getChannelForMessage(message).createMessage("Wrong number").block();
if (number < 1 || number >= 10) {
message.respond("Wrong number");
return this;
}
final var heroName = IcyVeinsParser.normalizeHeroName(availableHeroes.get(number - 1));
showHeroBuildToDiscord(message, heroName);
final var heroName = availableHeroes.get(number - 1).word;
showHeroBuildToDiscord(message, heroName, talentProvider);
return new DefaultChatState();
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
package org.tbplusc.app.discordinteraction;

import discord4j.core.object.entity.Message;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class MessageHandler {
private final Map<String, ChatState> states = new HashMap<>();
private final ExecutorService threadPool;

public void handleMessage(Message message) {
final var thread = new Thread(() -> processMessage(message),
"Discord command processor");
thread.start();
public MessageHandler() {
this.threadPool = Executors.newFixedThreadPool(24);
}

private void processMessage(Message message) {
final var authorOptional = message.getAuthor();
if (authorOptional.isEmpty()) {
throw new NullPointerException("Message had no author");
}
final var authorId = authorOptional.get().getId();
final var channel = message.getChannel().block();
if (channel == null) {
throw new NullPointerException("No channel for the message");
}
final var channelId = channel.getId();
final var key = authorId.asString() + channelId.asString();
public Future<?> handleMessage(WrappedMessage message) {
return threadPool.submit(() -> processMessage(message));
}

private void processMessage(WrappedMessage message) {
final var key = message.getContextKey();
if (!states.containsKey(key)) {
states.put(key, new DefaultChatState());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.tbplusc.app.discordinteraction;

import discord4j.core.object.entity.Message;
import static org.tbplusc.app.discordinteraction.DiscordUtil.getChannelForMessage;

public class WrappedDiscordMessage implements WrappedMessage {
private final Message message;

public WrappedDiscordMessage(Message message) {
this.message = message;
}

@Override
public String getContextKey() {
final var authorOptional = message.getAuthor();
if (authorOptional.isEmpty()) {
throw new NullPointerException("Message had no author");
}
final var authorId = authorOptional.get().getId();
final var channel = message.getChannel().block();
if (channel == null) {
throw new NullPointerException("No channel for the message");
}
final var channelId = channel.getId();
return authorId.asString() + channelId.asString();
}

@Override public String getContent() {
return message.getContent();
}

@Override public void respond(String text) {
var channel = getChannelForMessage(message);
channel.createMessage(text).block();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.tbplusc.app.discordinteraction;

public interface WrappedMessage {
String getContextKey();

String getContent();

void respond(String text);
}
Loading

0 comments on commit 225f46a

Please sign in to comment.