diff --git a/.gitignore b/.gitignore index 401e106f..83b71098 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,9 @@ /out /streamers /target +bin/ *.iml /analytics.db /config.json +/log4j2.xml diff --git a/README.adoc b/README.adoc index bd9002e5..97e1dbca 100644 --- a/README.adoc +++ b/README.adoc @@ -15,7 +15,7 @@ Other forms of point, like those provided by bots through IRC (Streamlabs, etc.) * Follow raids. * Participate and claim link:https://www.twitch.tv/drops/campaigns[campaigns]. * Join IRC chat. -* Participate in predictions. +* Participate in predictions and record peoples predictions. Limits: @@ -224,9 +224,10 @@ Analytics settings define a way to collect data on your twitch account as time p This includes: * Balance evolution -* Predictions made & results +* Your own Predictions made & results +* Predictions from other chat participants and their return-on-investment (only approximate as the bet amount is mostly anonymous). -These are store in an external database which allows any external software to access it and process it they wanted way. +These are stored in an external database which allows any external software to access it and process it the wanted way. Several database types are supported and listed below. Each database (logical database for MariaDB, different file for H2/SQLite) will however represent one mined account. @@ -243,6 +244,10 @@ You'll therefore have to adjust the settings for each mined account to not point |database |<>. | + +|recordChatsPredictions +|If set to true, other peoples predictions will be recorded. This is done from two sources, from the top-predictors list and from chat messages (via their badge). Reading from chat, requires the joinIrc setting to be set to true. +|false |=== ==== Analytics database [[analytics_database_settings]] @@ -270,6 +275,12 @@ NOTE: Can be omitted if no account NOTE: Can be omitted if no password | + +|maxPoolSize +|Maximum number of connections to the database. + +NOTE: For SQLite connection, value 1 will be forced. +|10 |=== === Streamer settings [[streamer_settings]] @@ -472,6 +483,15 @@ This is the same as "the outcome with higher odds". |Outcome with the person that placed the biggest prediction overall. | +|mostTrusted +|Choose the outcome that's backed by other users with the highest average return-on-investment. *Requires at least some data gathered beforehand via the 'recordChatsPredictions' setting!* E.g. only users with at least 5 made predictions are taken into account by default. +|Take the return-on-investment from people who already placed a prediction, calculate the average per outcome, then pick the highest. +|* minTotalBetsPlacedByUser: only user with at least this number of bets are considered in the calculation. Default is 5. + +* minTotalBetsPlacedOnPrediction: if not enough bets were placed, skip this prediction. Default is 10. + +* minTotalBetsPlacedOnOutcome: if not enough bets were placed on the chosen outcome, skip this prediction. Default is 5. + |smart |Choose the outcome with the most users. However, if the two most picked outcomes have a user count similar, choose the outcome with the least points (higher odds). diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ITwitchChatClient.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ITwitchChatClient.java index 50cf9e2f..46219908 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ITwitchChatClient.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ITwitchChatClient.java @@ -13,4 +13,6 @@ public interface ITwitchChatClient extends AutoCloseable{ @Override void close(); + + void addChatMessageListener(@NotNull ITwitchChatMessageListener listener); } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ITwitchChatMessageListener.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ITwitchChatMessageListener.java new file mode 100644 index 00000000..82729edd --- /dev/null +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ITwitchChatMessageListener.java @@ -0,0 +1,10 @@ +package fr.raksrinana.channelpointsminer.miner.api.chat; + +import org.jetbrains.annotations.NotNull; + +public interface ITwitchChatMessageListener{ + + void onChatMessage(@NotNull String streamer, @NotNull String actor, @NotNull String message); + + void onChatMessage(@NotNull String streamer, @NotNull String actor, @NotNull String message, @NotNull String badges); +} diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/TwitchChatEventProducer.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/TwitchChatEventProducer.java new file mode 100644 index 00000000..aa1acea5 --- /dev/null +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/TwitchChatEventProducer.java @@ -0,0 +1,24 @@ +package fr.raksrinana.channelpointsminer.miner.api.chat; + +import fr.raksrinana.channelpointsminer.miner.event.impl.ChatMessageEvent; +import fr.raksrinana.channelpointsminer.miner.factory.TimeFactory; +import fr.raksrinana.channelpointsminer.miner.miner.IMiner; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; + +@RequiredArgsConstructor +public class TwitchChatEventProducer implements ITwitchChatMessageListener{ + @NotNull + private final IMiner miner; + + @Override + public void onChatMessage(@NotNull String streamer, @NotNull String actor, @NotNull String message){ + onChatMessage(streamer, actor, message, ""); + } + + @Override + public void onChatMessage(@NotNull String streamer, @NotNull String actor, @NotNull String message, @NotNull String badges){ + var event = new ChatMessageEvent(miner, TimeFactory.now(), streamer, actor, message, badges); + miner.onEvent(event); + } +} diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/TwitchChatFactory.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/TwitchChatFactory.java deleted file mode 100644 index 1135124d..00000000 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/TwitchChatFactory.java +++ /dev/null @@ -1,30 +0,0 @@ -package fr.raksrinana.channelpointsminer.miner.api.chat; - -import fr.raksrinana.channelpointsminer.miner.api.chat.irc.TwitchIrcChatClient; -import fr.raksrinana.channelpointsminer.miner.api.chat.ws.TwitchChatWebSocketPool; -import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin; -import fr.raksrinana.channelpointsminer.miner.config.ChatMode; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import org.jetbrains.annotations.NotNull; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class TwitchChatFactory{ - @NotNull - public static ITwitchChatClient createChat(@NotNull ChatMode chatMode, @NotNull TwitchLogin twitchLogin){ - return switch(chatMode){ - case IRC -> createIrcChat(twitchLogin); - case WS -> createWsChat(twitchLogin); - }; - } - - @NotNull - private static ITwitchChatClient createIrcChat(@NotNull TwitchLogin twitchLogin){ - return new TwitchIrcChatClient(twitchLogin); - } - - @NotNull - private static ITwitchChatClient createWsChat(@NotNull TwitchLogin twitchLogin){ - return new TwitchChatWebSocketPool(Integer.MAX_VALUE, twitchLogin); - } -} diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcChatClient.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcChatClient.java index 91794277..019de670 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcChatClient.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcChatClient.java @@ -1,12 +1,16 @@ package fr.raksrinana.channelpointsminer.miner.api.chat.irc; import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatClient; +import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatMessageListener; import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.kitteh.irc.client.library.Client; +import org.kitteh.irc.client.library.defaults.element.messagetag.DefaultMessageTagLabel; +import java.util.Collection; +import java.util.LinkedList; import java.util.Locale; import java.util.Objects; import java.util.Optional; @@ -15,11 +19,18 @@ @Log4j2 public class TwitchIrcChatClient implements ITwitchChatClient{ + private static final String TAGS_CAPABILITY = "twitch.tv/tags"; + private static final String EMOTE_SETS_TAG_NAME = "emote-sets"; + @NotNull private final TwitchLogin twitchLogin; + private final boolean listenMessages; + private final Collection chatMessageListeners = new LinkedList<>(); @Nullable private Client ircClient; + @Nullable + private TwitchIrcMessageHandler ircMessageHandler; @Override public void join(@NotNull String channel){ @@ -38,20 +49,10 @@ public void join(@NotNull String channel){ public void joinPending(){ } - private synchronized Client getIrcClient(){ - if(Objects.isNull(ircClient)){ - log.info("Creating new Twitch IRC client"); - - ircClient = TwitchIrcFactory.createIrcClient(twitchLogin); - ircClient.connect(); - ircClient.setExceptionListener(e -> log.error("Error from irc", e)); - - ircClient.getEventManager().registerEventListener(TwitchIrcFactory.createIrcListener(twitchLogin.getUsername())); - - log.info("IRC Client created"); - } - - return ircClient; + @Override + public void addChatMessageListener(@NotNull ITwitchChatMessageListener listener){ + chatMessageListeners.add(listener); + Optional.ofNullable(ircMessageHandler).ifPresent(i -> i.addListener(listener)); } @Override @@ -79,4 +80,35 @@ public void ping(){ public void close(){ Optional.ofNullable(ircClient).ifPresent(Client::shutdown); } + + @NotNull + private synchronized Client getIrcClient(){ + if(Objects.isNull(ircClient)){ + log.info("Creating new Twitch IRC client"); + + ircClient = TwitchIrcFactory.createIrcClient(twitchLogin); + ircClient.connect(); + ircClient.setExceptionListener(e -> log.error("Error from irc", e)); + + var eventManager = ircClient.getEventManager(); + eventManager.registerEventListener(TwitchIrcFactory.createIrcConnectionHandler(twitchLogin.getUsername())); + + if(listenMessages){ + ircMessageHandler = TwitchIrcFactory.createIrcMessageHandler(twitchLogin.getUsername()); + chatMessageListeners.forEach(ircMessageHandler::addListener); + eventManager.registerEventListener(ircMessageHandler); + + var capabilityRequest = ircClient.commands().capabilityRequest(); + capabilityRequest.enable(TAGS_CAPABILITY); + capabilityRequest.execute(); + + var tagManager = ircClient.getMessageTagManager(); + tagManager.registerTagCreator(TAGS_CAPABILITY, EMOTE_SETS_TAG_NAME, DefaultMessageTagLabel.FUNCTION); + } + + log.info("IRC Client created"); + } + + return ircClient; + } } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcEventListener.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcConnectionHandler.java similarity index 96% rename from miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcEventListener.java rename to miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcConnectionHandler.java index 77e67537..94d9e782 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcEventListener.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcConnectionHandler.java @@ -11,7 +11,9 @@ @RequiredArgsConstructor @Log4j2 -public class TwitchIrcEventListener{ +public class TwitchIrcConnectionHandler{ + + @NotNull private final String accountName; @Handler @@ -20,7 +22,7 @@ private void onClientConnectionEstablishedEvent(ClientNegotiationCompleteEvent e log.info("IRC client connected"); } } - + @Handler public void onClientConnectionCLoseEvent(ClientConnectionClosedEvent event){ try(var ignored = LogContext.with(accountName)){ @@ -33,7 +35,7 @@ public void onClientConnectionCLoseEvent(ClientConnectionClosedEvent event){ } } } - + @Handler public void onChannelJoinEvent(@NotNull RequestedChannelJoinCompleteEvent event){ try(var ignored = LogContext.with(accountName)){ diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcFactory.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcFactory.java index 416d8d32..4145c02a 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcFactory.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcFactory.java @@ -12,12 +12,12 @@ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class TwitchIrcFactory{ private static final String TWITCH_IRC_HOST = "irc.chat.twitch.tv"; - - @NotNull - public static Client createIrcClient(@NotNull TwitchLogin twitchLogin){ - return createIrcClient(twitchLogin.getUsername(), "oauth:%s".formatted(twitchLogin.getAccessToken())); - } - + + @NotNull + public static Client createIrcClient(@NotNull TwitchLogin twitchLogin){ + return createIrcClient(twitchLogin.getUsername(), "oauth:%s".formatted(twitchLogin.getAccessToken())); + } + @NotNull private static Client createIrcClient(@NotNull String username, @Nullable String password){ var client = Client.builder() @@ -31,7 +31,12 @@ private static Client createIrcClient(@NotNull String username, @Nullable String } @NotNull - public static TwitchIrcEventListener createIrcListener(@NotNull String accountName){ - return new TwitchIrcEventListener(accountName); + public static TwitchIrcConnectionHandler createIrcConnectionHandler(@NotNull String accountName){ + return new TwitchIrcConnectionHandler(accountName); + } + + @NotNull + public static TwitchIrcMessageHandler createIrcMessageHandler(@NotNull String accountName){ + return new TwitchIrcMessageHandler(accountName); } } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcMessageHandler.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcMessageHandler.java new file mode 100644 index 00000000..a1d0c0f7 --- /dev/null +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcMessageHandler.java @@ -0,0 +1,47 @@ +package fr.raksrinana.channelpointsminer.miner.api.chat.irc; + +import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatMessageListener; +import fr.raksrinana.channelpointsminer.miner.log.LogContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import net.engio.mbassy.listener.Handler; +import org.jetbrains.annotations.NotNull; +import org.kitteh.irc.client.library.event.channel.ChannelMessageEvent; +import java.util.Collection; +import java.util.LinkedList; + +@RequiredArgsConstructor +@Log4j2 +public class TwitchIrcMessageHandler{ + + @NotNull + private final String accountName; + + @NotNull + private final Collection listeners = new LinkedList<>(); + + @Handler + public void onMessageEvent(@NotNull ChannelMessageEvent event){ + try(var ignored = LogContext.with(accountName)){ + log.trace("Received Irc Chat Message"); + var badges = event.getTag("badges"); + if(badges.isPresent()){ + listeners.forEach(l -> l.onChatMessage( + event.getChannel().getName().substring(1), + event.getActor().getMessagingName(), + event.getMessage(), + badges.get().getAsString())); + } + else{ + listeners.forEach(l -> l.onChatMessage( + event.getChannel().getName().substring(1), + event.getActor().getMessagingName(), + event.getMessage())); + } + } + } + + public void addListener(ITwitchChatMessageListener listener){ + listeners.add(listener); + } +} diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/ITwitchChatWebSocketListener.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/ITwitchChatWebSocketClosedListener.java similarity index 83% rename from miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/ITwitchChatWebSocketListener.java rename to miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/ITwitchChatWebSocketClosedListener.java index daed24f4..37591ccc 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/ITwitchChatWebSocketListener.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/ITwitchChatWebSocketClosedListener.java @@ -3,6 +3,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public interface ITwitchChatWebSocketListener{ +public interface ITwitchChatWebSocketClosedListener{ void onWebSocketClosed(@NotNull TwitchChatWebSocketClient client, int code, @Nullable String reason, boolean remote); } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketClient.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketClient.java index 9675cb12..7b2f7d05 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketClient.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketClient.java @@ -1,10 +1,13 @@ package fr.raksrinana.channelpointsminer.miner.api.chat.ws; import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatClient; +import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatMessageListener; import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin; import fr.raksrinana.channelpointsminer.miner.factory.TimeFactory; import fr.raksrinana.channelpointsminer.miner.log.LogContext; +import lombok.AccessLevel; import lombok.Getter; +import lombok.Setter; import lombok.extern.log4j.Log4j2; import org.java_websocket.WebSocket; import org.java_websocket.client.WebSocketClient; @@ -12,34 +15,44 @@ import org.java_websocket.framing.PongFrame; import org.java_websocket.handshake.ServerHandshake; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.TestOnly; import java.net.URI; import java.time.Instant; import java.util.Collection; import java.util.HashSet; +import java.util.LinkedList; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.regex.Pattern; @Log4j2 public class TwitchChatWebSocketClient extends WebSocketClient implements ITwitchChatClient{ + private final static Pattern MESSAGE_PATTERN = Pattern.compile("badges=([^;]*);.*display-name=([^;]*);.*PRIVMSG #([^ ]*) :(.*)"); + @Getter private final Set channels; - private final Collection listeners; + private final Collection socketClosedListeners; @Getter private final String uuid; private final TwitchLogin twitchLogin; + private final Collection chatMessageListeners; + @Setter(onMethod_ = {@TestOnly}, value = AccessLevel.PROTECTED) + private boolean listenMessages; @Getter private Instant lastHeartbeat; - public TwitchChatWebSocketClient(@NotNull URI uri, @NotNull TwitchLogin twitchLogin){ + public TwitchChatWebSocketClient(@NotNull URI uri, @NotNull TwitchLogin twitchLogin, boolean listenMessages){ super(uri); this.twitchLogin = twitchLogin; + this.listenMessages = listenMessages; uuid = UUID.randomUUID().toString(); setConnectionLostTimeout(0); channels = new HashSet<>(); - listeners = new ConcurrentLinkedQueue<>(); + socketClosedListeners = new ConcurrentLinkedQueue<>(); + chatMessageListeners = new LinkedList<>(); lastHeartbeat = Instant.EPOCH; } @@ -56,11 +69,21 @@ public void onOpen(ServerHandshake serverHandshake){ @Override public void onMessage(String messageStr){ - try(var logContext = LogContext.empty().withSocketId(uuid)){ + try(var ignored = LogContext.empty().withSocketId(uuid)){ log.trace("Received Chat Websocket message: {}", messageStr.strip()); if(messageStr.startsWith("PONG :tmi.twitch.tv")){ onWebsocketPong(this, new PongFrame()); } + else if(listenMessages){ + var messageMatch = MESSAGE_PATTERN.matcher(messageStr); + if(messageMatch.find()){ + var badges = messageMatch.group(1); + var actor = messageMatch.group(2); + var streamer = messageMatch.group(3); + var message = messageMatch.group(4); + chatMessageListeners.forEach(l -> l.onChatMessage(streamer, actor, message, badges)); + } + } } catch(Exception e){ log.error("Failed to handle Chat WebSocket message {}", messageStr, e); @@ -71,7 +94,7 @@ public void onMessage(String messageStr){ public void onClose(int code, String reason, boolean remote){ try(var ignored = LogContext.empty().withSocketId(uuid)){ log.info("Chat WebSocket closed with code {}, from host {}, reason {}", code, remote, reason); - listeners.forEach(l -> l.onWebSocketClosed(this, code, reason, remote)); + socketClosedListeners.forEach(l -> l.onWebSocketClosed(this, code, reason, remote)); } } @@ -129,12 +152,17 @@ public void ping(){ send("PING"); } + @Override + public void addChatMessageListener(@NotNull ITwitchChatMessageListener listener){ + chatMessageListeners.add(listener); + } + public boolean isChannelJoined(@NotNull String channel){ return channels.contains(channel); } - public void addListener(ITwitchChatWebSocketListener listener){ - listeners.add(listener); + public void addWebSocketClosedListener(@NotNull ITwitchChatWebSocketClosedListener listener){ + socketClosedListeners.add(listener); } public long getChannelCount(){ diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketPool.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketPool.java index b0988d6a..ad01e92a 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketPool.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketPool.java @@ -1,6 +1,7 @@ package fr.raksrinana.channelpointsminer.miner.api.chat.ws; import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatClient; +import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatMessageListener; import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin; import fr.raksrinana.channelpointsminer.miner.factory.TimeFactory; import fr.raksrinana.channelpointsminer.miner.factory.TwitchWebSocketClientFactory; @@ -9,6 +10,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collection; +import java.util.LinkedList; import java.util.Objects; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; @@ -17,19 +19,23 @@ import static org.java_websocket.framing.CloseFrame.NORMAL; @Log4j2 -public class TwitchChatWebSocketPool implements AutoCloseable, ITwitchChatWebSocketListener, ITwitchChatClient{ +public class TwitchChatWebSocketPool implements AutoCloseable, ITwitchChatWebSocketClosedListener, ITwitchChatClient{ private static final int SOCKET_TIMEOUT_MINUTES = 5; private final Collection clients; private final int maxTopicPerClient; + private final boolean listenMessages; + private final Collection chatMessageListeners; private final TwitchLogin twitchLogin; private final Queue pendingJoin; - public TwitchChatWebSocketPool(int maxTopicPerClient, @NotNull TwitchLogin twitchLogin){ + public TwitchChatWebSocketPool(int maxTopicPerClient, @NotNull TwitchLogin twitchLogin, boolean listenMessages){ this.maxTopicPerClient = maxTopicPerClient; this.twitchLogin = twitchLogin; + this.listenMessages = listenMessages; clients = new ConcurrentLinkedQueue<>(); pendingJoin = new ConcurrentLinkedQueue<>(); + chatMessageListeners = new LinkedList<>(); } @Override @@ -111,9 +117,10 @@ private TwitchChatWebSocketClient getAvailableClient(){ @NotNull private TwitchChatWebSocketClient createNewClient(){ try{ - var client = TwitchWebSocketClientFactory.createChatClient(twitchLogin); + var client = TwitchWebSocketClientFactory.createChatClient(twitchLogin, listenMessages); log.debug("Created websocket client with uuid {}", client.getUuid()); - client.addListener(this); + client.addWebSocketClosedListener(this); + chatMessageListeners.forEach(client::addChatMessageListener); client.connectBlocking(); clients.add(client); return client; @@ -132,4 +139,10 @@ public void close(){ public int getClientCount(){ return clients.size(); } + + @Override + public void addChatMessageListener(@NotNull ITwitchChatMessageListener listener){ + chatMessageListeners.add(listener); + clients.forEach(c -> c.addChatMessageListener(listener)); + } } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/ITwitchPubSubMessageListener.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/ITwitchPubSubMessageListener.java index 339d4682..d976fb76 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/ITwitchPubSubMessageListener.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/ITwitchPubSubMessageListener.java @@ -1,9 +1,9 @@ package fr.raksrinana.channelpointsminer.miner.api.ws; -import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IMessage; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IPubSubMessage; import fr.raksrinana.channelpointsminer.miner.api.ws.data.request.topic.Topic; import org.jetbrains.annotations.NotNull; public interface ITwitchPubSubMessageListener{ - void onTwitchMessage(@NotNull Topic topic, @NotNull IMessage message); + void onTwitchMessage(@NotNull Topic topic, @NotNull IPubSubMessage message); } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ChannelLastViewedContentUpdated.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ChannelLastViewedContentUpdated.java index 7a129d51..d87a11c1 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ChannelLastViewedContentUpdated.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ChannelLastViewedContentUpdated.java @@ -11,7 +11,7 @@ @Getter @EqualsAndHashCode(callSuper = true) @ToString -public class ChannelLastViewedContentUpdated extends IMessage{ +public class ChannelLastViewedContentUpdated extends IPubSubMessage{ @JsonProperty("data") private ChannelLastViewedContentUpdatedData data; } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ClaimAvailable.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ClaimAvailable.java index 7e9c6782..6ad2d048 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ClaimAvailable.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ClaimAvailable.java @@ -18,7 +18,7 @@ @EqualsAndHashCode(callSuper = true) @ToString @Builder -public class ClaimAvailable extends IMessage{ +public class ClaimAvailable extends IPubSubMessage{ @JsonProperty("data") @NotNull private ClaimAvailableData data; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ClaimClaimed.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ClaimClaimed.java index 418ec793..ca7f0009 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ClaimClaimed.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ClaimClaimed.java @@ -18,7 +18,7 @@ @EqualsAndHashCode(callSuper = true) @ToString @Builder -public class ClaimClaimed extends IMessage{ +public class ClaimClaimed extends IPubSubMessage{ @JsonProperty("data") @NotNull private ClaimClaimedData data; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/Commercial.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/Commercial.java index 9d9f8f6b..4dfbd39b 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/Commercial.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/Commercial.java @@ -20,7 +20,7 @@ @AllArgsConstructor @NoArgsConstructor @Builder -public class Commercial extends IMessage{ +public class Commercial extends IPubSubMessage{ @JsonProperty("server_time") @JsonDeserialize(using = TwitchTimestampDeserializer.class) @NotNull diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/CreateNotification.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/CreateNotification.java index 18929fb2..0f88ad86 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/CreateNotification.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/CreateNotification.java @@ -18,7 +18,7 @@ @AllArgsConstructor @NoArgsConstructor @Builder -public class CreateNotification extends IMessage{ +public class CreateNotification extends IPubSubMessage{ @JsonProperty("data") @NotNull private CreateNotificationData data; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/DeleteNotification.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/DeleteNotification.java index fe6e1564..0181e930 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/DeleteNotification.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/DeleteNotification.java @@ -18,7 +18,7 @@ @AllArgsConstructor @NoArgsConstructor @Builder -public class DeleteNotification extends IMessage{ +public class DeleteNotification extends IPubSubMessage{ @JsonProperty("data") @NotNull private DeleteNotificationData data; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/EventCreated.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/EventCreated.java index 208630fb..cd76047f 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/EventCreated.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/EventCreated.java @@ -11,7 +11,7 @@ @Getter @EqualsAndHashCode(callSuper = true) @ToString -public class EventCreated extends IMessage{ +public class EventCreated extends IPubSubMessage{ @JsonProperty("data") private EventCreatedData data; } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/EventUpdated.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/EventUpdated.java index 894945c3..02e21313 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/EventUpdated.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/EventUpdated.java @@ -11,7 +11,7 @@ @Getter @EqualsAndHashCode(callSuper = true) @ToString -public class EventUpdated extends IMessage{ +public class EventUpdated extends IPubSubMessage{ @JsonProperty("data") private EventUpdatedData data; } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/GlobalLastViewedContentUpdated.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/GlobalLastViewedContentUpdated.java index 5132dac6..cb3c0a93 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/GlobalLastViewedContentUpdated.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/GlobalLastViewedContentUpdated.java @@ -11,7 +11,7 @@ @Getter @EqualsAndHashCode(callSuper = true) @ToString -public class GlobalLastViewedContentUpdated extends IMessage{ +public class GlobalLastViewedContentUpdated extends IPubSubMessage{ @JsonProperty("data") private GlobalLastViewedContentUpdatedData data; } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/IMessage.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/IPubSubMessage.java similarity index 98% rename from miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/IMessage.java rename to miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/IPubSubMessage.java index 66d70b99..cc2359ed 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/IMessage.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/IPubSubMessage.java @@ -36,5 +36,5 @@ }) @EqualsAndHashCode @ToString -public abstract class IMessage{ +public abstract class IPubSubMessage{ } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PointsEarned.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PointsEarned.java index 277b3d96..1c6ec0be 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PointsEarned.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PointsEarned.java @@ -17,7 +17,7 @@ @AllArgsConstructor @NoArgsConstructor @Builder -public class PointsEarned extends IMessage{ +public class PointsEarned extends IPubSubMessage{ @JsonProperty("data") private PointsEarnedData data; } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PointsSpent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PointsSpent.java index 4dbd5357..4075afa6 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PointsSpent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PointsSpent.java @@ -17,7 +17,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class PointsSpent extends IMessage{ +public class PointsSpent extends IPubSubMessage{ @JsonProperty("data") private PointsSpentData data; } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PredictionMade.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PredictionMade.java index 7e6f440d..efa3df4a 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PredictionMade.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PredictionMade.java @@ -17,7 +17,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class PredictionMade extends IMessage{ +public class PredictionMade extends IPubSubMessage{ @JsonProperty("data") private PredictionMadeData data; } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PredictionResult.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PredictionResult.java index 541fd41e..a87c20b1 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PredictionResult.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PredictionResult.java @@ -18,7 +18,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class PredictionResult extends IMessage{ +public class PredictionResult extends IPubSubMessage{ @JsonProperty("data") @NotNull private PredictionResultData data; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PredictionUpdated.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PredictionUpdated.java index a76a80d7..098ca02f 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PredictionUpdated.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/PredictionUpdated.java @@ -17,7 +17,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class PredictionUpdated extends IMessage{ +public class PredictionUpdated extends IPubSubMessage{ @JsonProperty("data") private PredictionUpdatedData data; } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/RaidCancelV2.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/RaidCancelV2.java index 2661d7e7..d712ccb3 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/RaidCancelV2.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/RaidCancelV2.java @@ -18,7 +18,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class RaidCancelV2 extends IMessage{ +public class RaidCancelV2 extends IPubSubMessage{ @JsonProperty("raid") @NotNull private Raid raid; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/RaidGoV2.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/RaidGoV2.java index d271dff1..a43f49ba 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/RaidGoV2.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/RaidGoV2.java @@ -18,7 +18,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class RaidGoV2 extends IMessage{ +public class RaidGoV2 extends IPubSubMessage{ @JsonProperty("raid") @NotNull private Raid raid; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/RaidUpdateV2.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/RaidUpdateV2.java index 8799e17f..038e2d19 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/RaidUpdateV2.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/RaidUpdateV2.java @@ -18,7 +18,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class RaidUpdateV2 extends IMessage{ +public class RaidUpdateV2 extends IPubSubMessage{ @JsonProperty("raid") @NotNull private Raid raid; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/StreamDown.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/StreamDown.java index 7ccb42cb..5eee9ed3 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/StreamDown.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/StreamDown.java @@ -13,7 +13,7 @@ @Getter @EqualsAndHashCode(callSuper = true) @ToString -public class StreamDown extends IMessage{ +public class StreamDown extends IPubSubMessage{ @JsonProperty("server_time") @JsonDeserialize(using = TwitchTimestampDeserializer.class) private Instant serverTime; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/StreamUp.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/StreamUp.java index 6ef0e3de..566c8039 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/StreamUp.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/StreamUp.java @@ -13,7 +13,7 @@ @Getter @EqualsAndHashCode(callSuper = true) @ToString -public class StreamUp extends IMessage{ +public class StreamUp extends IPubSubMessage{ @JsonProperty("server_time") @JsonDeserialize(using = TwitchTimestampDeserializer.class) private Instant serverTime; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/UpdateSummary.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/UpdateSummary.java index 289d8cce..06f4e220 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/UpdateSummary.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/UpdateSummary.java @@ -18,7 +18,7 @@ @AllArgsConstructor @NoArgsConstructor @Builder -public class UpdateSummary extends IMessage{ +public class UpdateSummary extends IPubSubMessage{ @JsonProperty("data") @NotNull private UpdateSummaryData data; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ViewCount.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ViewCount.java index 39ee03a6..cbdd4fbc 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ViewCount.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/ViewCount.java @@ -19,7 +19,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class ViewCount extends IMessage{ +public class ViewCount extends IPubSubMessage{ @JsonProperty("server_time") @JsonDeserialize(using = TwitchTimestampDeserializer.class) private Instant serverTime; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/WatchPartyVod.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/WatchPartyVod.java index 437b1344..a6bca6e2 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/WatchPartyVod.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/message/WatchPartyVod.java @@ -18,7 +18,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class WatchPartyVod extends IMessage{ +public class WatchPartyVod extends IPubSubMessage{ @JsonProperty("vod") @NotNull private Vod vod; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/response/MessageData.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/response/MessageData.java index fdc65263..c763a74d 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/response/MessageData.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/response/MessageData.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; -import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IMessage; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IPubSubMessage; import fr.raksrinana.channelpointsminer.miner.api.ws.data.request.topic.Topic; import fr.raksrinana.channelpointsminer.miner.util.json.JacksonUtils; import lombok.AllArgsConstructor; @@ -25,7 +25,7 @@ public class MessageData{ private Topic topic; @JsonIgnore - private IMessage message; + private IPubSubMessage message; @JsonProperty("message") public void setMessage(String value) throws IOException{ diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/config/AnalyticsConfiguration.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/config/AnalyticsConfiguration.java index 8eb5254b..6f96f9ca 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/config/AnalyticsConfiguration.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/config/AnalyticsConfiguration.java @@ -22,4 +22,8 @@ public class AnalyticsConfiguration{ @Comment(value = "Database settings") @Nullable private DatabaseConfiguration database; + @JsonProperty("recordChatsPredictions") + @Comment(value = "Record other chat members predictions.") + @Builder.Default + private boolean recordChatsPredictions = false; } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/config/DatabaseConfiguration.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/config/DatabaseConfiguration.java index 12da5865..f09f9b33 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/config/DatabaseConfiguration.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/config/DatabaseConfiguration.java @@ -28,4 +28,8 @@ public class DatabaseConfiguration{ @Nullable @ToString.Exclude private String password; + @JsonProperty("maxPoolSize") + @Comment(value = "Max pool size") + @Builder.Default + private int maxPoolSize = 10; } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/BaseDatabase.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/BaseDatabase.java index 8417efca..1d1c1d72 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/BaseDatabase.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/BaseDatabase.java @@ -1,16 +1,28 @@ package fr.raksrinana.channelpointsminer.miner.database; import com.zaxxer.hikari.HikariDataSource; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; +import fr.raksrinana.channelpointsminer.miner.database.converter.Converters; +import fr.raksrinana.channelpointsminer.miner.database.model.prediction.OutcomeStatistic; +import fr.raksrinana.channelpointsminer.miner.factory.TimeFactory; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.SQLException; import java.time.Instant; import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Locale; +import java.util.Optional; import static java.time.ZoneOffset.UTC; @RequiredArgsConstructor +@Log4j2 public abstract class BaseDatabase implements IDatabase{ private final HikariDataSource dataSource; @@ -67,14 +79,210 @@ public void addPrediction(@NotNull String channelId, @NotNull String eventId, @N } } + @Override + public void addUserPrediction(@NotNull String username, @NotNull String streamerName, @NotNull String badge) throws SQLException{ + + try(var conn = getConnection(); + var selectUserStatement = conn.prepareStatement(""" + SELECT `ID` FROM `PredictionUser` WHERE `Username`=?"""); + var predictionStatement = getPredictionStmt(conn)){ + conn.setAutoCommit(false); + + username = username.toLowerCase(Locale.ROOT); + + selectUserStatement.setString(1, username); + var userResult = selectUserStatement.executeQuery(); + int userId; + if(userResult.next()){ + userId = userResult.getInt(1); + } + else{ + try(var addUserStatement = conn.prepareStatement(""" + INSERT INTO `PredictionUser`(`Username`) VALUES (?) RETURNING `ID`""")){ + addUserStatement.setString(1, username); + var insertResult = addUserStatement.executeQuery(); + insertResult.next(); + userId = insertResult.getInt(1); + } + log.debug("Added new prediction user '{}'", username); + } + + predictionStatement.setInt(1, userId); + predictionStatement.setString(2, badge); + predictionStatement.setString(3, streamerName); + + predictionStatement.executeUpdate(); + conn.commit(); + } + } + + @NotNull + protected abstract PreparedStatement getPredictionStmt(@NotNull Connection conn) throws SQLException; + + @Override + public void cancelPrediction(@NotNull Event event) throws SQLException{ + var ended = Optional.ofNullable(event.getEndedAt()).map(ZonedDateTime::toInstant).orElseGet(TimeFactory::now); + + try(var conn = getConnection(); + var addCanceledPredictionStmt = conn.prepareStatement(""" + INSERT INTO `ResolvedPrediction`(`EventID`,`ChannelID`, `Title`,`EventCreated`,`EventEnded`,`Canceled`) + VALUES (?,?,?,?,?,true)"""); + var removePredictionsStmt = getDeleteUserPredictionsForChannelStmt(conn) + ){ + conn.setAutoCommit(false); + try{ + //Add canceled prediction + addCanceledPredictionStmt.setString(1, event.getId()); + addCanceledPredictionStmt.setString(2, event.getChannelId()); + addCanceledPredictionStmt.setString(3, event.getTitle()); + addCanceledPredictionStmt.setObject(4, event.getCreatedAt().withZoneSameInstant(UTC).toLocalDateTime()); + addCanceledPredictionStmt.setObject(5, LocalDateTime.ofInstant(ended, UTC)); + addCanceledPredictionStmt.executeUpdate(); + + //Remove made predictions + removePredictionsStmt.setString(1, event.getChannelId()); + removePredictionsStmt.executeUpdate(); + + conn.commit(); + } + catch(SQLException e){ + conn.rollback(); + throw e; + } + } + } + + @Override + public void resolvePrediction(@NotNull Event event, @NotNull String outcome, @NotNull String badge, double returnRatioForWin) throws SQLException{ + var ended = Optional.ofNullable(event.getEndedAt()).map(ZonedDateTime::toInstant).orElseGet(TimeFactory::now); + + try(var conn = getConnection(); + var getOpenPredictionStmt = conn.prepareStatement(""" + SELECT * FROM `UserPrediction` WHERE `ChannelID`=?"""); + var updatePredictionUserStmt = getUpdatePredictionUserStmt(conn); + var addResolvedPredictionStmt = conn.prepareStatement(""" + INSERT INTO `ResolvedPrediction`(`EventID`,`ChannelID`, `Title`,`EventCreated`,`EventEnded`,`Canceled`,`Outcome`,`Badge`,`ReturnRatioForWin`) + VALUES (?,?,?,?,?,false,?,?,?)"""); + var removePredictionsStmt = getDeleteUserPredictionsForChannelStmt(conn) + ){ + conn.setAutoCommit(false); + + try{ + //Get user predictions, determine win/lose and update + double returnOnInvestment = returnRatioForWin - 1; + getOpenPredictionStmt.setString(1, event.getChannelId()); + try(var result = getOpenPredictionStmt.executeQuery()){ + while(result.next()){ + var userPrediction = Converters.convertUserPrediction(result); + if(badge.equals(userPrediction.getBadge())){ + updatePredictionUserStmt.setInt(1, 1); + updatePredictionUserStmt.setDouble(2, returnOnInvestment); + } + else{ + updatePredictionUserStmt.setInt(1, 0); + updatePredictionUserStmt.setDouble(2, -1); + } + updatePredictionUserStmt.setInt(3, userPrediction.getUserId()); + updatePredictionUserStmt.addBatch(); + } + updatePredictionUserStmt.executeBatch(); + } + + //Add the resolved prediction + addResolvedPredictionStmt.setString(1, event.getId()); + addResolvedPredictionStmt.setString(2, event.getChannelId()); + addResolvedPredictionStmt.setString(3, event.getTitle()); + addResolvedPredictionStmt.setObject(4, event.getCreatedAt().withZoneSameInstant(UTC).toLocalDateTime()); + addResolvedPredictionStmt.setObject(5, LocalDateTime.ofInstant(ended, UTC)); + addResolvedPredictionStmt.setString(6, outcome); + addResolvedPredictionStmt.setString(7, badge); + addResolvedPredictionStmt.setDouble(8, returnRatioForWin); + addResolvedPredictionStmt.executeUpdate(); + + //Remove Predictions + removePredictionsStmt.setString(1, event.getChannelId()); + removePredictionsStmt.executeUpdate(); + + conn.commit(); + } + catch(SQLException e){ + conn.rollback(); + throw e; + } + } + } + + @NotNull + protected abstract PreparedStatement getUpdatePredictionUserStmt(@NotNull Connection conn) throws SQLException; + + @Override + public void deleteUserPredictions() throws SQLException{ + log.debug("Removing all user predictions."); + try(var conn = getConnection(); + var statement = conn.prepareStatement(""" + DELETE FROM `UserPrediction`""" + )){ + statement.executeUpdate(); + } + } + + @NotNull + private PreparedStatement getDeleteUserPredictionsForChannelStmt(@NotNull Connection conn) throws SQLException{ + return conn.prepareStatement(""" + DELETE FROM `UserPrediction` + WHERE `ChannelID`=?""" + ); + } + + @Override + public void deleteUserPredictionsForChannel(@NotNull String channelId) throws SQLException{ + log.debug("Removing user predictions for channelId '{}'.", channelId); + try(var conn = getConnection(); var statement = getDeleteUserPredictionsForChannelStmt(conn)){ + statement.setString(1, channelId); + + statement.executeUpdate(); + } + } + + @Override + @NotNull + public Collection getOutcomeStatisticsForChannel(@NotNull String channelId, int minBetsPlacedByUser) throws SQLException{ + log.debug("Getting most trusted prediction from already placed bets."); + try(var conn = getConnection(); + var statement = conn.prepareStatement(""" + SELECT `Badge`, + COUNT(`UserID`) AS UserCnt, + AVG(`WinRate`) AS AvgWinRate, + AVG(`PredictionCnt`) AS AvgUserBetsPlaced, + AVG(`WinCnt`) AS AvgUserWins, + AVG(`ReturnOnInvestment`) AS AvgReturnOnInvestment + FROM `UserPrediction` AS up + INNER JOIN `PredictionUser` AS pu ON up.`UserID`=pu.`ID` + WHERE `ChannelID`=? + AND `PredictionCnt`>? + GROUP BY `Badge`""" + )){ + statement.setString(1, channelId); + statement.setInt(2, minBetsPlacedByUser); + + var outcomeStatistics = new LinkedList(); + try(var result = statement.executeQuery()){ + while(result.next()){ + outcomeStatistics.add(Converters.convertOutcomeTrust(result)); + } + } + + return outcomeStatistics; + } + } + @Override public void close(){ dataSource.close(); } @NotNull - @Override - public Connection getConnection() throws SQLException{ + protected Connection getConnection() throws SQLException{ return dataSource.getConnection(); } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/DatabaseEventHandler.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/DatabaseEventHandler.java new file mode 100644 index 00000000..ce8c62d1 --- /dev/null +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/DatabaseEventHandler.java @@ -0,0 +1,151 @@ +package fr.raksrinana.channelpointsminer.miner.database; + +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.pointsearned.Balance; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.EventStatus; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; +import fr.raksrinana.channelpointsminer.miner.event.EventHandlerAdapter; +import fr.raksrinana.channelpointsminer.miner.event.IStreamerEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.ChatMessageEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.EventCreatedEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.EventUpdatedEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.PointsEarnedEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.PointsSpentEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.PredictionMadeEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.PredictionResultEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.StreamDownEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.StreamUpEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.StreamerAddedEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.sql.SQLException; +import java.util.regex.Pattern; + +@RequiredArgsConstructor +@Log4j2 +public class DatabaseEventHandler extends EventHandlerAdapter{ + + private final static Pattern CHAT_PREDICTION_BADGE_PATTERN = Pattern.compile("predictions/([^,]*)"); + + @NotNull + private IDatabase database; + + @Override + public void onEventCreatedEvent(@NotNull EventCreatedEvent event) throws Exception{ + database.deleteUserPredictionsForChannel(event.getStreamerUsername().orElseThrow()); + } + + @Override + public void onEventUpdatedEvent(@NotNull EventUpdatedEvent event) throws Exception{ + var streamerUsername = event.getStreamerUsername(); + var predictionEvent = event.getEvent(); + + if(predictionEvent.getStatus() == EventStatus.ACTIVE){ + log.debug("Prediction-Update: Event ACTIVE. Streamer: {}, Title: {}", streamerUsername, predictionEvent.getTitle()); + for(var outcome : predictionEvent.getOutcomes()){ + var badge = outcome.getBadge().getVersion(); + for(var predictor : outcome.getTopPredictors()){ + database.addUserPrediction(predictor.getUserDisplayName(), streamerUsername, badge); + } + } + } + else if(predictionEvent.getStatus() == EventStatus.CANCELED){ + log.info("Prediction-Update: Event CANCELED. Streamer: {}, Title: {}", streamerUsername, predictionEvent.getTitle()); + database.cancelPrediction(predictionEvent); + } + else if(predictionEvent.getStatus() == EventStatus.RESOLVED){ + var winningOutcomeId = predictionEvent.getWinningOutcomeId(); + var winningOutcome = predictionEvent.getOutcomes().stream() + .filter(e -> e.getId().equals(winningOutcomeId)) + .findAny().orElseThrow(); + var winningOutcomeBadge = winningOutcome.getBadge().getVersion(); + + log.info("Prediction-Update: Event RESOLVED. Streamer: {}, Title: {}, Outcome: {}", streamerUsername, predictionEvent.getTitle(), winningOutcome.getTitle()); + + var totalPoints = predictionEvent.getOutcomes().stream().mapToDouble(Outcome::getTotalPoints).sum(); + var returnRatio = totalPoints / winningOutcome.getTotalPoints(); + database.resolvePrediction(predictionEvent, winningOutcome.getTitle(), winningOutcomeBadge, returnRatio); + } + } + + @Override + public void onPointsEarnedEvent(@NotNull PointsEarnedEvent event) throws SQLException{ + var pointsEarnedData = event.getPointsEarnedData(); + var reasonCode = pointsEarnedData.getPointGain().getReasonCode(); + updateBalance(event, pointsEarnedData.getBalance(), reasonCode.name()); + } + + @Override + public void onPointsSpentEvent(@NotNull PointsSpentEvent event) throws SQLException{ + updateBalance(event, event.getPointsSpentData().getBalance(), null); + } + + @Override + public void onPredictionMadeEvent(@NotNull PredictionMadeEvent event) throws SQLException{ + var placedPrediction = event.getPlacedPrediction(); + addPrediction(event, placedPrediction.getEventId(), "PREDICTED", Integer.toString(placedPrediction.getAmount())); + + if(placedPrediction.getBettingPrediction() != null && event.getStreamerUsername().isPresent()){ + var outcomeId = placedPrediction.getOutcomeId(); + var placedOutcome = placedPrediction.getBettingPrediction().getEvent().getOutcomes().stream() + .filter(o -> o.getId().equals(outcomeId)) + .findFirst(); + if(placedOutcome.isPresent()){ + database.addUserPrediction(event.getMiner().getUsername(), event.getStreamerUsername().get(), placedOutcome.get().getBadge().getVersion()); + } + } + } + + @Override + public void onPredictionResultEvent(@NotNull PredictionResultEvent event) throws Exception{ + addPrediction(event, event.getPredictionResultData().getPrediction().getEventId(), "RESULT", event.getGain()); + } + + @Override + public void onStreamDownEvent(@NotNull StreamDownEvent event) throws SQLException{ + updateStreamer(event); + } + + @Override + public void onStreamerAddedEvent(@NotNull StreamerAddedEvent event) throws SQLException{ + database.createChannel(event.getStreamerId(), event.getStreamerUsername().orElseThrow(() -> new IllegalStateException("No username present in streamer"))); + } + + @Override + public void onStreamUpEvent(@NotNull StreamUpEvent event) throws SQLException{ + updateStreamer(event); + } + + @Override + public void onChatMessageEvent(@NotNull ChatMessageEvent event){ + var matcher = CHAT_PREDICTION_BADGE_PATTERN.matcher(event.getBadges()); + if(matcher.find()){ + try{ + var predictionBadge = matcher.group(1); + log.debug("Read user prediction from chat. User: {}, Badge: {}", event.getActor(), predictionBadge); + database.addUserPrediction(event.getActor(), event.getStreamer(), predictionBadge); + } + catch(SQLException e){ + log.error("SQL Exception while adding user prediction: {}", e.getMessage()); + } + } + } + + private void updateStreamer(@NotNull IStreamerEvent event) throws SQLException{ + database.updateChannelStatusTime(event.getStreamerId(), event.getInstant()); + } + + private void addPrediction(@NotNull IStreamerEvent event, @NotNull String eventId, @NotNull String type, @NotNull String description) throws SQLException{ + database.addPrediction(event.getStreamerId(), eventId, type, description, event.getInstant()); + } + + private void updateBalance(@NotNull IStreamerEvent event, @NotNull Balance balance, @Nullable String reason) throws SQLException{ + database.addBalance(event.getStreamerId(), balance.getBalance(), reason, event.getInstant()); + } + + @Override + public void close(){ + database.close(); + } +} diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/DatabaseHandler.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/DatabaseHandler.java deleted file mode 100644 index 85774878..00000000 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/DatabaseHandler.java +++ /dev/null @@ -1,79 +0,0 @@ -package fr.raksrinana.channelpointsminer.miner.database; - -import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.pointsearned.Balance; -import fr.raksrinana.channelpointsminer.miner.event.IEvent; -import fr.raksrinana.channelpointsminer.miner.event.IEventListener; -import fr.raksrinana.channelpointsminer.miner.event.IStreamerEvent; -import fr.raksrinana.channelpointsminer.miner.event.impl.PointsEarnedEvent; -import fr.raksrinana.channelpointsminer.miner.event.impl.PointsSpentEvent; -import fr.raksrinana.channelpointsminer.miner.event.impl.PredictionMadeEvent; -import fr.raksrinana.channelpointsminer.miner.event.impl.PredictionResultEvent; -import fr.raksrinana.channelpointsminer.miner.event.impl.StreamDownEvent; -import fr.raksrinana.channelpointsminer.miner.event.impl.StreamUpEvent; -import fr.raksrinana.channelpointsminer.miner.event.impl.StreamerAddedEvent; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import java.sql.SQLException; - -@RequiredArgsConstructor -@Log4j2 -public class DatabaseHandler implements IEventListener{ - @NotNull - private IDatabase database; - - @Override - public void onEvent(IEvent event){ - try{ - if(event instanceof StreamerAddedEvent e){ - addStreamer(e); - } - else if(event instanceof StreamUpEvent e){ - updateStreamer(e); - } - else if(event instanceof StreamDownEvent e){ - updateStreamer(e); - } - else if(event instanceof PointsEarnedEvent e){ - var pointsEarnedData = e.getPointsEarnedData(); - var reasonCode = pointsEarnedData.getPointGain().getReasonCode(); - updateBalance(e, pointsEarnedData.getBalance(), reasonCode.name()); - } - else if(event instanceof PointsSpentEvent e){ - updateBalance(e, e.getPointsSpentData().getBalance(), null); - } - else if(event instanceof PredictionMadeEvent e){ - var placedPrediction = e.getPlacedPrediction(); - addPrediction(e, placedPrediction.getEventId(), "PREDICTED", Integer.toString(placedPrediction.getAmount())); - } - else if(event instanceof PredictionResultEvent e){ - addPrediction(e, e.getPredictionResultData().getPrediction().getEventId(), "RESULT", e.getGain()); - } - } - catch(Exception e){ - log.error("Failed to process database event", e); - } - } - - private void addStreamer(@NotNull IStreamerEvent event) throws SQLException{ - database.createChannel(event.getStreamerId(), event.getStreamerUsername().orElseThrow(() -> new IllegalStateException("No username present in streamer"))); - } - - private void updateStreamer(@NotNull IStreamerEvent event) throws SQLException{ - database.updateChannelStatusTime(event.getStreamerId(), event.getInstant()); - } - - private void updateBalance(@NotNull IStreamerEvent event, @NotNull Balance balance, @Nullable String reason) throws SQLException{ - database.addBalance(event.getStreamerId(), balance.getBalance(), reason, event.getInstant()); - } - - private void addPrediction(@NotNull IStreamerEvent event, @NotNull String eventId, @NotNull String type, @NotNull String description) throws SQLException{ - database.addPrediction(event.getStreamerId(), eventId, type, description, event.getInstant()); - } - - @Override - public void close(){ - database.close(); - } -} diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/IDatabase.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/IDatabase.java index bff5ed91..89732e2f 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/IDatabase.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/IDatabase.java @@ -1,10 +1,12 @@ package fr.raksrinana.channelpointsminer.miner.database; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; +import fr.raksrinana.channelpointsminer.miner.database.model.prediction.OutcomeStatistic; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.sql.Connection; import java.sql.SQLException; import java.time.Instant; +import java.util.Collection; public interface IDatabase extends AutoCloseable{ void initDatabase() throws SQLException; @@ -17,9 +19,19 @@ public interface IDatabase extends AutoCloseable{ void addPrediction(@NotNull String channelId, @NotNull String eventId, @NotNull String type, @NotNull String description, @NotNull Instant instant) throws SQLException; - @Override - void close(); + void addUserPrediction(@NotNull String username, @NotNull String streamerName, @NotNull String badge) throws SQLException; + + void cancelPrediction(@NotNull Event event) throws SQLException; + + void resolvePrediction(@NotNull Event event, @NotNull String outcome, @NotNull String badge, double returnOnInvestment) throws SQLException; + + void deleteUserPredictions() throws SQLException; + + void deleteUserPredictionsForChannel(@NotNull String channelId) throws SQLException; @NotNull - Connection getConnection() throws SQLException; + Collection getOutcomeStatisticsForChannel(@NotNull String channelId, int minBetsPlacedByUser) throws SQLException; + + @Override + void close(); } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/MariaDBDatabase.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/MariaDBDatabase.java index 5db6955b..482a8f46 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/MariaDBDatabase.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/MariaDBDatabase.java @@ -2,6 +2,8 @@ import com.zaxxer.hikari.HikariDataSource; import org.jetbrains.annotations.NotNull; +import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.SQLException; public class MariaDBDatabase extends BaseDatabase{ @@ -14,20 +16,21 @@ public void initDatabase() throws SQLException{ execute(""" CREATE TABLE IF NOT EXISTS `Channel` ( `ID` VARCHAR(32) NOT NULL PRIMARY KEY, - `Username` VARCHAR(128) NOT NULL, - `LastStatusChange` DATETIME NOT NULL + `Username` VARCHAR(128) NOT NULL, + `LastStatusChange` DATETIME NOT NULL, + INDEX `UsernameIdx`(`Username`) ) - ENGINE=InnoDB DEFAULT CHARSET=utf8;""", + ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""", """ CREATE TABLE IF NOT EXISTS `Balance` ( `ID` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `ChannelID` VARCHAR(32) NOT NULL REFERENCES `Channel`(`ID`), - `BalanceDate` DATETIME(3) NOT NULL, - `Balance` INT NOT NULL, - `Reason` VARCHAR(16) NULL, - INDEX `PointsDateIdx`(`BalanceDate`) + `BalanceDate` DATETIME(3) NOT NULL, + `Balance` INT NOT NULL, + `Reason` VARCHAR(16) NULL, + INDEX `PointsDateIdx`(`BalanceDate`) ) - ENGINE=InnoDB DEFAULT CHARSET=utf8;""", + ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""", """ CREATE TABLE IF NOT EXISTS `Prediction` ( `ID` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, @@ -39,7 +42,43 @@ public void initDatabase() throws SQLException{ INDEX `EventDateIdx`(`EventDate`), INDEX `EventTypeIdx`(`Type`) ) - ENGINE=InnoDB DEFAULT CHARSET=utf8;"""); + ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""", + """ + CREATE TABLE IF NOT EXISTS `ResolvedPrediction` ( + `EventID` VARCHAR(36) NOT NULL PRIMARY KEY, + `ChannelID` VARCHAR(32) NOT NULL REFERENCES `Channel`(`ID`), + `Title` VARCHAR(64) NOT NULL, + `EventCreated` DATETIME NOT NULL, + `EventEnded` DATETIME NULL, + `Canceled` BOOLEAN NOT NULL, + `Outcome` VARCHAR(32) NULL, + `Badge` VARCHAR(32) NULL, + `ReturnRatioForWin` DOUBLE NULL, + INDEX `ChannelIDIdx`(`ChannelID`) + ) + ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""", + """ + CREATE TABLE IF NOT EXISTS `PredictionUser` ( + `ID` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `Username` VARCHAR(128) NOT NULL, + `PredictionCnt` SMALLINT UNSIGNED NOT NULL DEFAULT 0, + `WinCnt` SMALLINT UNSIGNED NOT NULL DEFAULT 0, + `WinRate` DECIMAL(8,7) NOT NULL DEFAULT 0, + `ReturnOnInvestment` DOUBLE NOT NULL DEFAULT 0, + UNIQUE (`Username`), + INDEX `UsernameIdx`(`Username`) + ) + ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;""", + """ + CREATE TABLE IF NOT EXISTS `UserPrediction` ( + `ChannelID` VARCHAR(32) NOT NULL REFERENCES `Channel`(`ID`), + `UserID` INT NOT NULL REFERENCES `PredictionUser`(`ID`), + `Badge` VARCHAR(32) NOT NULL, + PRIMARY KEY (`ChannelID`, `UserID`), + INDEX `ChannelIDIdx`(`ChannelID`), + INDEX `UserIDIdx`(`UserID`) + ) + ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"""); } @Override @@ -56,4 +95,22 @@ public void createChannel(@NotNull String channelId, @NotNull String username) t statement.executeUpdate(); } } -} + + @NotNull + @Override + protected PreparedStatement getPredictionStmt(@NotNull Connection conn) throws SQLException{ + return conn.prepareStatement(""" + INSERT IGNORE INTO `UserPrediction`(`ChannelID`, `UserID`, `Badge`) + SELECT c.`ID`, ?, ? FROM `Channel` AS c WHERE c.`Username`=?""" + ); + } + + @NotNull + @Override + protected PreparedStatement getUpdatePredictionUserStmt(@NotNull Connection conn) throws SQLException{ + return conn.prepareStatement(""" + UPDATE `PredictionUser` + SET `PredictionCnt`=`PredictionCnt`+1, `WinCnt`=`WinCnt`+?, `WinRate`=`WinCnt`/`PredictionCnt`, + `ReturnOnInvestment`=`ReturnOnInvestment`+? WHERE `ID`=?"""); + } +} \ No newline at end of file diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/NoOpDatabase.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/NoOpDatabase.java new file mode 100644 index 00000000..209cf635 --- /dev/null +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/NoOpDatabase.java @@ -0,0 +1,61 @@ +package fr.raksrinana.channelpointsminer.miner.database; + +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; +import fr.raksrinana.channelpointsminer.miner.database.model.prediction.OutcomeStatistic; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.time.Instant; +import java.util.Collection; +import java.util.List; + +public class NoOpDatabase implements IDatabase{ + @Override + public void initDatabase(){ + } + + @Override + public void createChannel(@NotNull String channelId, @NotNull String username){ + } + + @Override + public void updateChannelStatusTime(@NotNull String channelId, @NotNull Instant instant){ + } + + @Override + public void addBalance(@NotNull String channelId, int balance, @Nullable String reason, @NotNull Instant instant){ + } + + @Override + public void addPrediction(@NotNull String channelId, @NotNull String eventId, @NotNull String type, @NotNull String description, @NotNull Instant instant){ + } + + @Override + public void addUserPrediction(@NotNull String username, @NotNull String streamerName, @NotNull String badge){ + } + + @Override + public void cancelPrediction(@NotNull Event event){ + } + + @Override + public void resolvePrediction(@NotNull Event event, @NotNull String outcome, @NotNull String badge, double returnOnInvestment){ + } + + @Override + public void deleteUserPredictions(){ + } + + @Override + public void deleteUserPredictionsForChannel(@NotNull String channelId){ + } + + @Override + @NotNull + public Collection getOutcomeStatisticsForChannel(@NotNull String channelId, int minBetsPlacedByUser){ + return List.of(); + } + + @Override + public void close(){ + } +} diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/SQLiteDatabase.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/SQLiteDatabase.java index ef5adca7..c0d050ce 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/SQLiteDatabase.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/SQLiteDatabase.java @@ -2,6 +2,8 @@ import com.zaxxer.hikari.HikariDataSource; import org.jetbrains.annotations.NotNull; +import java.sql.Connection; +import java.sql.PreparedStatement; import java.sql.SQLException; import java.time.LocalDateTime; import static java.time.ZoneOffset.UTC; @@ -41,7 +43,44 @@ public void initDatabase() throws SQLException{ """ CREATE INDEX IF NOT EXISTS `EventDateIdx` ON `Prediction`(`EventDate`);""", """ - CREATE INDEX IF NOT EXISTS `EventTypeIdx` ON `Prediction`(`Type`);"""); + CREATE INDEX IF NOT EXISTS `EventTypeIdx` ON `Prediction`(`Type`);""", + """ + CREATE TABLE IF NOT EXISTS `ResolvedPrediction` ( + `EventID` VARCHAR(36) NOT NULL PRIMARY KEY, + `ChannelID` VARCHAR(32) NOT NULL REFERENCES `Channel`(`ID`), + `Title` VARCHAR(64) NOT NULL, + `EventCreated` DATETIME NOT NULL, + `EventEnded` DATETIME NULL, + `Canceled` BOOLEAN NOT NULL, + `Outcome` VARCHAR(32) NULL, + `Badge` VARCHAR(32) NULL, + `ReturnRatioForWin` REAL NULL + );""", + """ + CREATE INDEX IF NOT EXISTS `ChannelIDIdx` ON `ResolvedPrediction`(`ChannelID`);""", + """ + CREATE TABLE IF NOT EXISTS `PredictionUser` ( + `ID` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + `Username` VARCHAR(128) NOT NULL, + `PredictionCnt` SMALLINT UNSIGNED NOT NULL DEFAULT 0, + `WinCnt` SMALLINT UNSIGNED NOT NULL DEFAULT 0, + `WinRate` REAL NOT NULL DEFAULT 0, + `ReturnOnInvestment` REAL NOT NULL DEFAULT 0, + UNIQUE (`Username`) + );""", + """ + CREATE INDEX IF NOT EXISTS `UsernameIdx` ON `PredictionUser`(`Username`);""", + """ + CREATE TABLE IF NOT EXISTS `UserPrediction` ( + `ChannelID` VARCHAR(32) NOT NULL REFERENCES `Channel`(`ID`), + `UserID` INTEGER NOT NULL REFERENCES `PredictionUser`(`ID`), + `Badge` VARCHAR(32) NOT NULL, + PRIMARY KEY (`ChannelID`, `UserID`) + );""", + """ + CREATE INDEX IF NOT EXISTS `ChannelIDIdx` ON `UserPrediction`(`ChannelID`);""", + """ + CREATE INDEX IF NOT EXISTS `UserIDIdx` ON `UserPrediction`(`UserID`);"""); } @Override @@ -61,4 +100,23 @@ public void createChannel(@NotNull String channelId, @NotNull String username) t statement.executeUpdate(); } } + + @NotNull + @Override + protected PreparedStatement getPredictionStmt(@NotNull Connection conn) throws SQLException{ + return conn.prepareStatement(""" + INSERT OR IGNORE INTO `UserPrediction`(`ChannelID`, `UserID`, `Badge`) + SELECT c.`ID`, ?, ? FROM `Channel` AS c WHERE c.`Username`=?""" + ); + } + + @NotNull + @Override + protected PreparedStatement getUpdatePredictionUserStmt(@NotNull Connection conn) throws SQLException{ + return conn.prepareStatement(""" + WITH wi AS (SELECT ? AS n) + UPDATE `PredictionUser` + SET `PredictionCnt`=`PredictionCnt`+1, `WinCnt`=`WinCnt`+wi.n, + `WinRate`=CAST((`WinCnt`+wi.n) AS REAL)/(`PredictionCnt`+1), `ReturnOnInvestment`=`ReturnOnInvestment`+? FROM wi WHERE `ID`=?"""); + } } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/converter/Converters.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/converter/Converters.java new file mode 100644 index 00000000..6bca490e --- /dev/null +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/converter/Converters.java @@ -0,0 +1,30 @@ +package fr.raksrinana.channelpointsminer.miner.database.converter; + +import fr.raksrinana.channelpointsminer.miner.database.model.prediction.OutcomeStatistic; +import fr.raksrinana.channelpointsminer.miner.database.model.prediction.UserPrediction; +import org.jetbrains.annotations.NotNull; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class Converters{ + @NotNull + public static UserPrediction convertUserPrediction(@NotNull ResultSet rs) throws SQLException{ + return UserPrediction.builder() + .userId(rs.getInt("UserID")) + .channelId(rs.getString("ChannelID")) + .badge(rs.getString("Badge")) + .build(); + } + + @NotNull + public static OutcomeStatistic convertOutcomeTrust(@NotNull ResultSet rs) throws SQLException{ + return OutcomeStatistic.builder() + .badge(rs.getString("Badge")) + .userCnt(rs.getInt("UserCnt")) + .averageWinRate(rs.getDouble("AvgWinRate")) + .averageUserBetsPlaced(rs.getDouble("AvgUserBetsPlaced")) + .averageUserWins(rs.getDouble("AvgUserWins")) + .averageReturnOnInvestment(rs.getDouble("AvgReturnOnInvestment")) + .build(); + } +} diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/model/prediction/OutcomeStatistic.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/model/prediction/OutcomeStatistic.java new file mode 100644 index 00000000..d400722a --- /dev/null +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/model/prediction/OutcomeStatistic.java @@ -0,0 +1,24 @@ +package fr.raksrinana.channelpointsminer.miner.database.model.prediction; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.jetbrains.annotations.NotNull; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@ToString +public class OutcomeStatistic{ + + @NotNull + private String badge; + private int userCnt; + private double averageWinRate; + private double averageUserBetsPlaced; + private double averageUserWins; + private double averageReturnOnInvestment; +} diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/model/prediction/UserPrediction.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/model/prediction/UserPrediction.java new file mode 100644 index 00000000..a16f9c30 --- /dev/null +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/database/model/prediction/UserPrediction.java @@ -0,0 +1,21 @@ +package fr.raksrinana.channelpointsminer.miner.database.model.prediction; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserPrediction{ + + @NotNull + private String channelId; + private int userId; + @NotNull + private String badge; +} diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/AbstractEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/AbstractLoggableEvent.java similarity index 97% rename from miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/AbstractEvent.java rename to miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/AbstractLoggableEvent.java index e8169217..325c24b3 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/AbstractEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/AbstractLoggableEvent.java @@ -22,7 +22,7 @@ @RequiredArgsConstructor @EqualsAndHashCode @ToString -public abstract class AbstractEvent implements IEvent, ILoggableEvent{ +public abstract class AbstractLoggableEvent implements IEvent, ILoggableEvent{ protected static final int COLOR_INFO = Color.CYAN.getRGB(); protected static final int COLOR_PREDICTION = Color.PINK.getRGB(); protected static final int COLOR_POINTS_WON = Color.GREEN.getRGB(); diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/AbstractStreamerEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/AbstractLoggableStreamerEvent.java similarity index 81% rename from miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/AbstractStreamerEvent.java rename to miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/AbstractLoggableStreamerEvent.java index 78eb7574..f63a9ff8 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/AbstractStreamerEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/AbstractLoggableStreamerEvent.java @@ -13,7 +13,7 @@ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) -public abstract class AbstractStreamerEvent extends AbstractEvent implements IStreamerEvent{ +public abstract class AbstractLoggableStreamerEvent extends AbstractLoggableEvent implements IStreamerEvent{ private static final String UNKNOWN_STREAMER = "UnknownStreamer"; @Getter @@ -21,11 +21,11 @@ public abstract class AbstractStreamerEvent extends AbstractEvent implements ISt private final String streamerUsername; private final Streamer streamer; - public AbstractStreamerEvent(@NotNull IMiner miner, @NotNull Streamer streamer, @NotNull Instant instant){ + public AbstractLoggableStreamerEvent(@NotNull IMiner miner, @NotNull Streamer streamer, @NotNull Instant instant){ this(miner, streamer.getId(), streamer.getUsername(), streamer, instant); } - public AbstractStreamerEvent(@NotNull IMiner miner, @NotNull String streamerId, @Nullable String streamerUsername, @Nullable Streamer streamer, @NotNull Instant instant){ + public AbstractLoggableStreamerEvent(@NotNull IMiner miner, @NotNull String streamerId, @Nullable String streamerUsername, @Nullable Streamer streamer, @NotNull Instant instant){ super(miner, instant); this.streamerId = streamerId; this.streamerUsername = streamerUsername; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/EventHandlerAdapter.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/EventHandlerAdapter.java new file mode 100644 index 00000000..8d8c5de0 --- /dev/null +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/EventHandlerAdapter.java @@ -0,0 +1,109 @@ +package fr.raksrinana.channelpointsminer.miner.event; + +import fr.raksrinana.channelpointsminer.miner.event.impl.ChatMessageEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.ClaimAvailableEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.DropClaimEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.EventCreatedEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.EventUpdatedEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.MinerStartedEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.PointsEarnedEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.PointsSpentEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.PredictionMadeEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.PredictionResultEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.StreamDownEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.StreamUpEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.StreamerAddedEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.StreamerRemovedEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.StreamerUnknownEvent; +import fr.raksrinana.channelpointsminer.miner.util.ClassWalker; +import lombok.extern.log4j.Log4j2; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@SuppressWarnings("unused") +@Log4j2 +public abstract class EventHandlerAdapter implements IEventHandler{ + private static final MethodHandles.Lookup lookup = MethodHandles.lookup(); + private static final ConcurrentMap, MethodHandle> methods = new ConcurrentHashMap<>(); + private static final Set> unresolved; + + public void onClaimAvailableEvent(@NotNull ClaimAvailableEvent event) throws Exception{} + + public void onDropClaimEvent(@NotNull DropClaimEvent event) throws Exception{} + + public void onEventCreatedEvent(@NotNull EventCreatedEvent event) throws Exception{} + + public void onEventUpdatedEvent(@NotNull EventUpdatedEvent event) throws Exception{} + + public void onMinerStartedEvent(@NotNull MinerStartedEvent event) throws Exception{} + + public void onPointsEarnedEvent(@NotNull PointsEarnedEvent event) throws Exception{} + + public void onPointsSpentEvent(@NotNull PointsSpentEvent event) throws Exception{} + + public void onPredictionMadeEvent(@NotNull PredictionMadeEvent event) throws Exception{} + + public void onPredictionResultEvent(@NotNull PredictionResultEvent event) throws Exception{} + + public void onStreamDownEvent(@NotNull StreamDownEvent event) throws Exception{} + + public void onStreamerAddedEvent(@NotNull StreamerAddedEvent event) throws Exception{} + + public void onStreamerRemovedEvent(@NotNull StreamerRemovedEvent event) throws Exception{} + + public void onStreamerUnknownEvent(@NotNull StreamerUnknownEvent event) throws Exception{} + + public void onStreamUpEvent(@NotNull StreamUpEvent event) throws Exception{} + + public void onChatMessageEvent(@NotNull ChatMessageEvent event) throws Exception{} + + public void onILoggableEvent(@NotNull ILoggableEvent event) throws Exception{} + + @Override + public void onEvent(@NotNull IEvent event){ + for(var clazz : ClassWalker.range(event.getClass(), IEvent.class)){ + if(unresolved.contains(clazz)){ + continue; + } + var methodHandle = methods.computeIfAbsent(clazz, EventHandlerAdapter::findMethod); + if(methodHandle == null){ + unresolved.add(clazz); + continue; + } + + try{ + methodHandle.invoke(this, event); + } + catch(Throwable throwable){ + log.error("EventHandler threw an exception", throwable); + } + } + } + + @Nullable + private static MethodHandle findMethod(@NotNull Class clazz){ + var name = clazz.getSimpleName(); + var type = MethodType.methodType(Void.TYPE, clazz); + try{ + name = "on" + name; + return lookup.findVirtual(EventHandlerAdapter.class, name, type); + } + catch(NoSuchMethodException | IllegalAccessException ignored){ + } // this means this is probably a custom event! + return null; + } + + static{ + unresolved = ConcurrentHashMap.newKeySet(); + Collections.addAll(unresolved, + Object.class // Objects aren't events + ); + } +} diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/IEventListener.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/IEventHandler.java similarity index 69% rename from miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/IEventListener.java rename to miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/IEventHandler.java index 33a06ec3..73eaab51 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/IEventListener.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/IEventHandler.java @@ -1,6 +1,6 @@ package fr.raksrinana.channelpointsminer.miner.event; -public interface IEventListener extends AutoCloseable{ +public interface IEventHandler extends AutoCloseable{ void onEvent(IEvent event); @Override diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/ChatMessageEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/ChatMessageEvent.java new file mode 100644 index 00000000..200e2f67 --- /dev/null +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/ChatMessageEvent.java @@ -0,0 +1,30 @@ +package fr.raksrinana.channelpointsminer.miner.event.impl; + +import fr.raksrinana.channelpointsminer.miner.event.IEvent; +import fr.raksrinana.channelpointsminer.miner.miner.IMiner; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.jetbrains.annotations.NotNull; +import java.time.Instant; + +@RequiredArgsConstructor +@EqualsAndHashCode +@ToString +@Getter +public class ChatMessageEvent implements IEvent{ + @NotNull + private final IMiner miner; + @NotNull + private final Instant instant; + + @NotNull + private final String streamer; + @NotNull + private final String actor; + @NotNull + private final String message; + @NotNull + private final String badges; +} diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/ClaimAvailableEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/ClaimAvailableEvent.java index 7aadaa26..2eb54bcc 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/ClaimAvailableEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/ClaimAvailableEvent.java @@ -1,6 +1,6 @@ package fr.raksrinana.channelpointsminer.miner.event.impl; -import fr.raksrinana.channelpointsminer.miner.event.AbstractStreamerEvent; +import fr.raksrinana.channelpointsminer.miner.event.AbstractLoggableStreamerEvent; import fr.raksrinana.channelpointsminer.miner.miner.IMiner; import fr.raksrinana.channelpointsminer.miner.streamer.Streamer; import lombok.EqualsAndHashCode; @@ -11,7 +11,7 @@ @EqualsAndHashCode(callSuper = true) @ToString -public class ClaimAvailableEvent extends AbstractStreamerEvent{ +public class ClaimAvailableEvent extends AbstractLoggableStreamerEvent{ public ClaimAvailableEvent(@NotNull IMiner miner, @NotNull String streamerId, @Nullable String streamerUsername, @Nullable Streamer streamer, @NotNull Instant instant){ super(miner, streamerId, streamerUsername, streamer, instant); } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/DropClaimEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/DropClaimEvent.java index e96fdc0c..ce428bb2 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/DropClaimEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/DropClaimEvent.java @@ -2,7 +2,7 @@ import fr.raksrinana.channelpointsminer.miner.api.discord.data.Field; import fr.raksrinana.channelpointsminer.miner.api.gql.data.types.TimeBasedDrop; -import fr.raksrinana.channelpointsminer.miner.event.AbstractEvent; +import fr.raksrinana.channelpointsminer.miner.event.AbstractLoggableEvent; import fr.raksrinana.channelpointsminer.miner.miner.IMiner; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -13,7 +13,7 @@ @EqualsAndHashCode(callSuper = true) @ToString -public class DropClaimEvent extends AbstractEvent{ +public class DropClaimEvent extends AbstractLoggableEvent{ private final TimeBasedDrop drop; public DropClaimEvent(@NotNull IMiner miner, @NotNull TimeBasedDrop drop, @NotNull Instant instant){ diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/EventCreatedEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/EventCreatedEvent.java index d5a73b3e..f9234001 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/EventCreatedEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/EventCreatedEvent.java @@ -2,7 +2,7 @@ import fr.raksrinana.channelpointsminer.miner.api.discord.data.Field; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; -import fr.raksrinana.channelpointsminer.miner.event.AbstractStreamerEvent; +import fr.raksrinana.channelpointsminer.miner.event.AbstractLoggableStreamerEvent; import fr.raksrinana.channelpointsminer.miner.miner.IMiner; import fr.raksrinana.channelpointsminer.miner.streamer.Streamer; import lombok.EqualsAndHashCode; @@ -13,7 +13,7 @@ @EqualsAndHashCode(callSuper = true) @ToString -public class EventCreatedEvent extends AbstractStreamerEvent{ +public class EventCreatedEvent extends AbstractLoggableStreamerEvent{ private final Event event; public EventCreatedEvent(@NotNull IMiner miner, @NotNull Streamer streamer, @NotNull Event event){ diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/EventUpdatedEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/EventUpdatedEvent.java new file mode 100644 index 00000000..28cdb7e9 --- /dev/null +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/EventUpdatedEvent.java @@ -0,0 +1,26 @@ +package fr.raksrinana.channelpointsminer.miner.event.impl; + +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; +import fr.raksrinana.channelpointsminer.miner.event.IEvent; +import fr.raksrinana.channelpointsminer.miner.miner.IMiner; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.jetbrains.annotations.NotNull; +import java.time.Instant; + +@RequiredArgsConstructor +@EqualsAndHashCode +@ToString +@Getter +public class EventUpdatedEvent implements IEvent{ + @NotNull + private final IMiner miner; + @NotNull + private final Instant instant; + @NotNull + private final String streamerUsername; + @NotNull + private final Event event; +} diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/MinerStartedEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/MinerStartedEvent.java index 03d595ec..596fa2b2 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/MinerStartedEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/MinerStartedEvent.java @@ -1,7 +1,7 @@ package fr.raksrinana.channelpointsminer.miner.event.impl; import fr.raksrinana.channelpointsminer.miner.api.discord.data.Field; -import fr.raksrinana.channelpointsminer.miner.event.AbstractEvent; +import fr.raksrinana.channelpointsminer.miner.event.AbstractLoggableEvent; import fr.raksrinana.channelpointsminer.miner.miner.IMiner; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -12,7 +12,7 @@ @EqualsAndHashCode(callSuper = true) @ToString -public class MinerStartedEvent extends AbstractEvent{ +public class MinerStartedEvent extends AbstractLoggableEvent{ private final String version; private final String commit; private final String branch; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PointsEarnedEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PointsEarnedEvent.java index 4ca739ec..f2149821 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PointsEarnedEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PointsEarnedEvent.java @@ -2,7 +2,7 @@ import fr.raksrinana.channelpointsminer.miner.api.discord.data.Field; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.pointsearned.PointsEarnedData; -import fr.raksrinana.channelpointsminer.miner.event.AbstractStreamerEvent; +import fr.raksrinana.channelpointsminer.miner.event.AbstractLoggableStreamerEvent; import fr.raksrinana.channelpointsminer.miner.miner.IMiner; import fr.raksrinana.channelpointsminer.miner.streamer.Streamer; import lombok.EqualsAndHashCode; @@ -15,7 +15,7 @@ @EqualsAndHashCode(callSuper = true) @ToString -public class PointsEarnedEvent extends AbstractStreamerEvent{ +public class PointsEarnedEvent extends AbstractLoggableStreamerEvent{ @Getter private final PointsEarnedData pointsEarnedData; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PointsSpentEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PointsSpentEvent.java index 9b0ac454..4cee4a82 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PointsSpentEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PointsSpentEvent.java @@ -2,7 +2,7 @@ import fr.raksrinana.channelpointsminer.miner.api.discord.data.Field; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.pointsspent.PointsSpentData; -import fr.raksrinana.channelpointsminer.miner.event.AbstractStreamerEvent; +import fr.raksrinana.channelpointsminer.miner.event.AbstractLoggableStreamerEvent; import fr.raksrinana.channelpointsminer.miner.miner.IMiner; import fr.raksrinana.channelpointsminer.miner.streamer.Streamer; import lombok.EqualsAndHashCode; @@ -15,7 +15,7 @@ @EqualsAndHashCode(callSuper = true) @ToString -public class PointsSpentEvent extends AbstractStreamerEvent{ +public class PointsSpentEvent extends AbstractLoggableStreamerEvent{ @Getter private final PointsSpentData pointsSpentData; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PredictionMadeEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PredictionMadeEvent.java index 829e8628..85d1f88b 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PredictionMadeEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PredictionMadeEvent.java @@ -2,7 +2,7 @@ import fr.raksrinana.channelpointsminer.miner.api.discord.data.Field; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; -import fr.raksrinana.channelpointsminer.miner.event.AbstractStreamerEvent; +import fr.raksrinana.channelpointsminer.miner.event.AbstractLoggableStreamerEvent; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.handler.data.PlacedPrediction; import fr.raksrinana.channelpointsminer.miner.miner.IMiner; @@ -19,7 +19,7 @@ @EqualsAndHashCode(callSuper = true) @ToString -public class PredictionMadeEvent extends AbstractStreamerEvent{ +public class PredictionMadeEvent extends AbstractLoggableStreamerEvent{ private static final String UNKNOWN_OUTCOME = "UnknownOutcome"; @Getter diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PredictionResultEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PredictionResultEvent.java index f6e65bc8..abcfa848 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PredictionResultEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/PredictionResultEvent.java @@ -4,7 +4,7 @@ import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.predictionresult.PredictionResultData; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.PredictionResultPayload; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.PredictionResultType; -import fr.raksrinana.channelpointsminer.miner.event.AbstractStreamerEvent; +import fr.raksrinana.channelpointsminer.miner.event.AbstractLoggableStreamerEvent; import fr.raksrinana.channelpointsminer.miner.handler.data.PlacedPrediction; import fr.raksrinana.channelpointsminer.miner.miner.IMiner; import fr.raksrinana.channelpointsminer.miner.streamer.Streamer; @@ -19,7 +19,7 @@ @EqualsAndHashCode(callSuper = true) @ToString -public class PredictionResultEvent extends AbstractStreamerEvent{ +public class PredictionResultEvent extends AbstractLoggableStreamerEvent{ private final PlacedPrediction placedPrediction; @Getter private final PredictionResultData predictionResultData; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamDownEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamDownEvent.java index 19199f36..32796ce6 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamDownEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamDownEvent.java @@ -1,6 +1,6 @@ package fr.raksrinana.channelpointsminer.miner.event.impl; -import fr.raksrinana.channelpointsminer.miner.event.AbstractStreamerEvent; +import fr.raksrinana.channelpointsminer.miner.event.AbstractLoggableStreamerEvent; import fr.raksrinana.channelpointsminer.miner.miner.IMiner; import fr.raksrinana.channelpointsminer.miner.streamer.Streamer; import lombok.EqualsAndHashCode; @@ -11,7 +11,7 @@ @EqualsAndHashCode(callSuper = true) @ToString -public class StreamDownEvent extends AbstractStreamerEvent{ +public class StreamDownEvent extends AbstractLoggableStreamerEvent{ public StreamDownEvent(@NotNull IMiner miner, @NotNull String streamerId, @Nullable String streamerUsername, @Nullable Streamer streamer, @NotNull Instant instant){ super(miner, streamerId, streamerUsername, streamer, instant); } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamUpEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamUpEvent.java index 01d99d61..8b45a8eb 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamUpEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamUpEvent.java @@ -1,6 +1,6 @@ package fr.raksrinana.channelpointsminer.miner.event.impl; -import fr.raksrinana.channelpointsminer.miner.event.AbstractStreamerEvent; +import fr.raksrinana.channelpointsminer.miner.event.AbstractLoggableStreamerEvent; import fr.raksrinana.channelpointsminer.miner.miner.IMiner; import fr.raksrinana.channelpointsminer.miner.streamer.Streamer; import lombok.EqualsAndHashCode; @@ -11,7 +11,7 @@ @EqualsAndHashCode(callSuper = true) @ToString -public class StreamUpEvent extends AbstractStreamerEvent{ +public class StreamUpEvent extends AbstractLoggableStreamerEvent{ public StreamUpEvent(@NotNull IMiner miner, @NotNull String streamerId, @Nullable String streamerUsername, @Nullable Streamer streamer, @NotNull Instant instant){ super(miner, streamerId, streamerUsername, streamer, instant); } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamerAddedEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamerAddedEvent.java index 26df3aff..75f4fb2d 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamerAddedEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamerAddedEvent.java @@ -1,6 +1,6 @@ package fr.raksrinana.channelpointsminer.miner.event.impl; -import fr.raksrinana.channelpointsminer.miner.event.AbstractStreamerEvent; +import fr.raksrinana.channelpointsminer.miner.event.AbstractLoggableStreamerEvent; import fr.raksrinana.channelpointsminer.miner.miner.IMiner; import fr.raksrinana.channelpointsminer.miner.streamer.Streamer; import lombok.EqualsAndHashCode; @@ -10,7 +10,7 @@ @EqualsAndHashCode(callSuper = true) @ToString -public class StreamerAddedEvent extends AbstractStreamerEvent{ +public class StreamerAddedEvent extends AbstractLoggableStreamerEvent{ public StreamerAddedEvent(@NotNull IMiner miner, @NotNull Streamer streamer, @NotNull Instant instant){ super(miner, streamer, instant); } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamerRemovedEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamerRemovedEvent.java index 54288a18..ed4ae6e4 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamerRemovedEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamerRemovedEvent.java @@ -1,6 +1,6 @@ package fr.raksrinana.channelpointsminer.miner.event.impl; -import fr.raksrinana.channelpointsminer.miner.event.AbstractStreamerEvent; +import fr.raksrinana.channelpointsminer.miner.event.AbstractLoggableStreamerEvent; import fr.raksrinana.channelpointsminer.miner.miner.IMiner; import fr.raksrinana.channelpointsminer.miner.streamer.Streamer; import lombok.EqualsAndHashCode; @@ -10,7 +10,7 @@ @EqualsAndHashCode(callSuper = true) @ToString -public class StreamerRemovedEvent extends AbstractStreamerEvent{ +public class StreamerRemovedEvent extends AbstractLoggableStreamerEvent{ public StreamerRemovedEvent(@NotNull IMiner miner, @NotNull Streamer streamer, @NotNull Instant instant){ super(miner, streamer, instant); } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamerUnknownEvent.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamerUnknownEvent.java index b7128a08..89d218b0 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamerUnknownEvent.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/event/impl/StreamerUnknownEvent.java @@ -1,7 +1,7 @@ package fr.raksrinana.channelpointsminer.miner.event.impl; import fr.raksrinana.channelpointsminer.miner.api.discord.data.Field; -import fr.raksrinana.channelpointsminer.miner.event.AbstractStreamerEvent; +import fr.raksrinana.channelpointsminer.miner.event.AbstractLoggableStreamerEvent; import fr.raksrinana.channelpointsminer.miner.miner.IMiner; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -12,7 +12,7 @@ @EqualsAndHashCode(callSuper = true) @ToString -public class StreamerUnknownEvent extends AbstractStreamerEvent{ +public class StreamerUnknownEvent extends AbstractLoggableStreamerEvent{ public StreamerUnknownEvent(@NotNull IMiner miner, @NotNull String streamerUsername, @NotNull Instant instant){ super(miner, "-1", streamerUsername, null, instant); } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/DatabaseFactory.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/DatabaseFactory.java index c5e925e4..43fd1b2f 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/DatabaseFactory.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/DatabaseFactory.java @@ -4,16 +4,24 @@ import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.pool.HikariPool; import fr.raksrinana.channelpointsminer.miner.config.DatabaseConfiguration; -import fr.raksrinana.channelpointsminer.miner.database.DatabaseHandler; +import fr.raksrinana.channelpointsminer.miner.database.DatabaseEventHandler; import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.database.MariaDBDatabase; +import fr.raksrinana.channelpointsminer.miner.database.NoOpDatabase; import fr.raksrinana.channelpointsminer.miner.database.SQLiteDatabase; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.sql.SQLException; +import java.util.Objects; public class DatabaseFactory{ + @NotNull - public static IDatabase createDatabase(@NotNull DatabaseConfiguration configuration) throws SQLException, HikariPool.PoolInitializationException{ + public static IDatabase createDatabase(@Nullable DatabaseConfiguration configuration) throws SQLException, HikariPool.PoolInitializationException{ + if(Objects.isNull(configuration)){ + return new NoOpDatabase(); + } + var jdbcUrl = configuration.getJdbcUrl(); var parts = jdbcUrl.split(":"); @@ -22,28 +30,29 @@ public static IDatabase createDatabase(@NotNull DatabaseConfiguration configurat } var database = switch(parts[1]){ - case "mariadb" -> new MariaDBDatabase(createDatasource(configuration, "org.mariadb.jdbc.Driver")); - case "sqlite" -> new SQLiteDatabase(createDatasource(configuration, "org.sqlite.JDBC")); + case "mariadb" -> new MariaDBDatabase(createDatasource(configuration, "org.mariadb.jdbc.Driver", configuration.getMaxPoolSize())); + case "sqlite" -> new SQLiteDatabase(createDatasource(configuration, "org.sqlite.JDBC", 1)); default -> throw new IllegalStateException("Unknown JDBC type " + parts[1]); }; database.initDatabase(); return database; } - + @NotNull - private static HikariDataSource createDatasource(@NotNull DatabaseConfiguration configuration, @NotNull String driver){ + private static HikariDataSource createDatasource(@NotNull DatabaseConfiguration configuration, @NotNull String driver, int maxPoolSize){ var poolConfiguration = new HikariConfig(); poolConfiguration.setJdbcUrl(configuration.getJdbcUrl()); poolConfiguration.setUsername(configuration.getUsername()); poolConfiguration.setPassword(configuration.getPassword()); poolConfiguration.setDriverClassName(driver); + poolConfiguration.setMaximumPoolSize(maxPoolSize); return new HikariDataSource(poolConfiguration); } @NotNull - public static DatabaseHandler createDatabaseHandler(@NotNull IDatabase database){ - return new DatabaseHandler(database); + public static DatabaseEventHandler createDatabaseHandler(@NotNull IDatabase database){ + return new DatabaseEventHandler(database); } } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/LogEventListenerFactory.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/LogEventListenerFactory.java index 26dd7098..72cdf571 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/LogEventListenerFactory.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/LogEventListenerFactory.java @@ -1,7 +1,7 @@ package fr.raksrinana.channelpointsminer.miner.factory; import fr.raksrinana.channelpointsminer.miner.api.discord.DiscordApi; -import fr.raksrinana.channelpointsminer.miner.event.IEventListener; +import fr.raksrinana.channelpointsminer.miner.event.IEventHandler; import fr.raksrinana.channelpointsminer.miner.log.DiscordEventListener; import fr.raksrinana.channelpointsminer.miner.log.LoggerEventListener; import lombok.NoArgsConstructor; @@ -11,12 +11,12 @@ @NoArgsConstructor(access = PRIVATE) public class LogEventListenerFactory{ @NotNull - public static IEventListener createLogger(){ + public static IEventHandler createLogger(){ return new LoggerEventListener(); } @NotNull - public static IEventListener createDiscordLogger(@NotNull DiscordApi discordApi, boolean useEmbeds){ + public static IEventHandler createDiscordLogger(@NotNull DiscordApi discordApi, boolean useEmbeds){ return new DiscordEventListener(discordApi, useEmbeds); } } \ No newline at end of file diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/MinerFactory.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/MinerFactory.java index 520b78c7..3033ec44 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/MinerFactory.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/MinerFactory.java @@ -15,40 +15,44 @@ public class MinerFactory{ @NotNull public static Miner create(@NotNull AccountConfiguration config){ - var miner = new Miner( - config, - ApiFactory.createPassportApi(config.getUsername(), config.getPassword(), config.getAuthenticationFolder(), config.isUse2Fa()), - new StreamerSettingsFactory(config), - new TwitchPubSubWebSocketPool(50), - Executors.newScheduledThreadPool(4), - Executors.newCachedThreadPool()); - - miner.addHandler(MessageHandlerFactory.createClaimAvailableHandler(miner)); - miner.addHandler(MessageHandlerFactory.createStreamStartEndHandler(miner)); - miner.addHandler(MessageHandlerFactory.createFollowRaidHandler(miner)); - miner.addHandler(MessageHandlerFactory.createPredictionsHandler(miner, BetPlacerFactory.created(miner))); - miner.addHandler(MessageHandlerFactory.createPointsHandler(miner)); - - miner.addEventListener(LogEventListenerFactory.createLogger()); - if(Objects.nonNull(config.getDiscord().getUrl())){ - var discordApi = ApiFactory.createdDiscordApi(config.getDiscord().getUrl()); - miner.addEventListener(LogEventListenerFactory.createDiscordLogger(discordApi, config.getDiscord().isEmbeds())); - } - - if(config.getAnalytics().isEnabled()){ + try{ var dbConfig = config.getAnalytics().getDatabase(); - if(Objects.isNull(dbConfig)){ - throw new IllegalStateException("Analytics is enabled but no database is defined"); - } - try{ - var database = DatabaseFactory.createDatabase(dbConfig); - miner.addEventListener(DatabaseFactory.createDatabaseHandler(database)); + var database = DatabaseFactory.createDatabase(dbConfig); + + var miner = new Miner( + config, + ApiFactory.createPassportApi(config.getUsername(), config.getPassword(), config.getAuthenticationFolder(), config.isUse2Fa()), + new StreamerSettingsFactory(config), + new TwitchPubSubWebSocketPool(50), + Executors.newScheduledThreadPool(4), + Executors.newCachedThreadPool(), + database); + + miner.addPubSubHandler(PubSubMessageHandlerFactory.createClaimAvailableHandler(miner)); + miner.addPubSubHandler(PubSubMessageHandlerFactory.createStreamStartEndHandler(miner)); + miner.addPubSubHandler(PubSubMessageHandlerFactory.createFollowRaidHandler(miner)); + miner.addPubSubHandler(PubSubMessageHandlerFactory.createPredictionsHandler(miner, BetPlacerFactory.created(miner))); + miner.addPubSubHandler(PubSubMessageHandlerFactory.createPointsHandler(miner)); + + miner.addEventHandler(LogEventListenerFactory.createLogger()); + if(Objects.nonNull(config.getDiscord().getUrl())){ + var discordApi = ApiFactory.createdDiscordApi(config.getDiscord().getUrl()); + miner.addEventHandler(LogEventListenerFactory.createDiscordLogger(discordApi, config.getDiscord().isEmbeds())); } - catch(SQLException | HikariPool.PoolInitializationException e){ - throw new IllegalStateException("Failed to set up database", e); + + if(config.getAnalytics().isEnabled()){ + if(Objects.isNull(dbConfig)){ + throw new IllegalStateException("Analytics is enabled but no database is defined"); + } + + database.deleteUserPredictions(); + miner.addEventHandler(DatabaseFactory.createDatabaseHandler(database)); } + + return miner; + } + catch(SQLException | HikariPool.PoolInitializationException e){ + throw new IllegalStateException("Failed to set up database", e); } - - return miner; } } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/MessageHandlerFactory.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/PubSubMessageHandlerFactory.java similarity index 63% rename from miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/MessageHandlerFactory.java rename to miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/PubSubMessageHandlerFactory.java index 2e4a5078..f0e08b19 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/MessageHandlerFactory.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/PubSubMessageHandlerFactory.java @@ -2,7 +2,7 @@ import fr.raksrinana.channelpointsminer.miner.handler.ClaimAvailableHandler; import fr.raksrinana.channelpointsminer.miner.handler.FollowRaidHandler; -import fr.raksrinana.channelpointsminer.miner.handler.IMessageHandler; +import fr.raksrinana.channelpointsminer.miner.handler.IPubSubMessageHandler; import fr.raksrinana.channelpointsminer.miner.handler.PointsHandler; import fr.raksrinana.channelpointsminer.miner.handler.PredictionsHandler; import fr.raksrinana.channelpointsminer.miner.handler.StreamStartEndHandler; @@ -13,29 +13,29 @@ import static lombok.AccessLevel.PRIVATE; @NoArgsConstructor(access = PRIVATE) -public class MessageHandlerFactory{ +public class PubSubMessageHandlerFactory{ @NotNull - public static IMessageHandler createClaimAvailableHandler(@NotNull IMiner miner){ + public static IPubSubMessageHandler createClaimAvailableHandler(@NotNull IMiner miner){ return new ClaimAvailableHandler(miner); } @NotNull - public static IMessageHandler createStreamStartEndHandler(@NotNull IMiner miner){ + public static IPubSubMessageHandler createStreamStartEndHandler(@NotNull IMiner miner){ return new StreamStartEndHandler(miner); } @NotNull - public static IMessageHandler createFollowRaidHandler(@NotNull IMiner miner){ + public static IPubSubMessageHandler createFollowRaidHandler(@NotNull IMiner miner){ return new FollowRaidHandler(miner); } @NotNull - public static IMessageHandler createPredictionsHandler(@NotNull IMiner miner, @NotNull BetPlacer betPlacer){ + public static IPubSubMessageHandler createPredictionsHandler(@NotNull IMiner miner, @NotNull BetPlacer betPlacer){ return new PredictionsHandler(miner, betPlacer); } @NotNull - public static IMessageHandler createPointsHandler(@NotNull IMiner miner){ + public static IPubSubMessageHandler createPointsHandler(@NotNull IMiner miner){ return new PointsHandler(miner); } } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/TwitchChatFactory.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/TwitchChatFactory.java new file mode 100644 index 00000000..dee10059 --- /dev/null +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/TwitchChatFactory.java @@ -0,0 +1,33 @@ +package fr.raksrinana.channelpointsminer.miner.factory; + +import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatClient; +import fr.raksrinana.channelpointsminer.miner.api.chat.irc.TwitchIrcChatClient; +import fr.raksrinana.channelpointsminer.miner.api.chat.ws.TwitchChatWebSocketPool; +import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin; +import fr.raksrinana.channelpointsminer.miner.config.ChatMode; +import fr.raksrinana.channelpointsminer.miner.miner.IMiner; +import org.jetbrains.annotations.NotNull; + +public class TwitchChatFactory{ + @NotNull + public static ITwitchChatClient createChat(@NotNull IMiner miner, @NotNull ChatMode chatMode, boolean listenMessages){ + var twitchLogin = miner.getTwitchLogin(); + + var chatClient = switch(chatMode){ + case IRC -> createIrcChat(twitchLogin, listenMessages); + case WS -> createWsChat(twitchLogin, listenMessages); + }; + + return chatClient; + } + + @NotNull + private static ITwitchChatClient createIrcChat(@NotNull TwitchLogin twitchLogin, boolean listenMessages){ + return new TwitchIrcChatClient(twitchLogin, listenMessages); + } + + @NotNull + private static ITwitchChatClient createWsChat(@NotNull TwitchLogin twitchLogin, boolean listenMessages){ + return new TwitchChatWebSocketPool(Integer.MAX_VALUE, twitchLogin, listenMessages); + } +} diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/TwitchWebSocketClientFactory.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/TwitchWebSocketClientFactory.java index 230a9026..f071c889 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/TwitchWebSocketClientFactory.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/factory/TwitchWebSocketClientFactory.java @@ -19,7 +19,7 @@ public static TwitchPubSubWebSocketClient createPubSubClient(){ } @NotNull - public static TwitchChatWebSocketClient createChatClient(@NotNull TwitchLogin twitchLogin){ - return new TwitchChatWebSocketClient(IRC_URI, twitchLogin); + public static TwitchChatWebSocketClient createChatClient(@NotNull TwitchLogin twitchLogin, boolean listenMessages){ + return new TwitchChatWebSocketClient(IRC_URI, twitchLogin, listenMessages); } } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/ClaimAvailableHandler.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/ClaimAvailableHandler.java index 1f930cc0..6259f916 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/ClaimAvailableHandler.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/ClaimAvailableHandler.java @@ -10,7 +10,7 @@ import java.util.Objects; @RequiredArgsConstructor -public class ClaimAvailableHandler extends HandlerAdapter{ +public class ClaimAvailableHandler extends PubSubMessageHandlerAdapter{ private final IMiner miner; @Override diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/FollowRaidHandler.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/FollowRaidHandler.java index 88b461db..3b7b1d74 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/FollowRaidHandler.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/FollowRaidHandler.java @@ -10,7 +10,7 @@ @RequiredArgsConstructor @Log4j2 -public class FollowRaidHandler extends HandlerAdapter{ +public class FollowRaidHandler extends PubSubMessageHandlerAdapter{ private final IMiner miner; @Override diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/IMessageHandler.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/IPubSubMessageHandler.java similarity index 65% rename from miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/IMessageHandler.java rename to miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/IPubSubMessageHandler.java index b1cc3e91..e4944e39 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/IMessageHandler.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/IPubSubMessageHandler.java @@ -1,9 +1,9 @@ package fr.raksrinana.channelpointsminer.miner.handler; -import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IMessage; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IPubSubMessage; import fr.raksrinana.channelpointsminer.miner.api.ws.data.request.topic.Topic; import org.jetbrains.annotations.NotNull; -public interface IMessageHandler{ - void handle(@NotNull Topic topic, @NotNull IMessage message); +public interface IPubSubMessageHandler{ + void handle(@NotNull Topic topic, @NotNull IPubSubMessage message); } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/NotificationHandler.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/NotificationHandler.java index 2b681fb2..f609a144 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/NotificationHandler.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/NotificationHandler.java @@ -9,7 +9,7 @@ import java.util.Objects; @RequiredArgsConstructor -public class NotificationHandler extends HandlerAdapter{ +public class NotificationHandler extends PubSubMessageHandlerAdapter{ public static final String DROP_REWARD_REMINDER = "user_drop_reward_reminder_notification"; private final IMiner miner; diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/PointsHandler.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/PointsHandler.java index ad6d597c..93226dcc 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/PointsHandler.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/PointsHandler.java @@ -11,7 +11,7 @@ import java.util.Objects; @RequiredArgsConstructor -public class PointsHandler extends HandlerAdapter{ +public class PointsHandler extends PubSubMessageHandlerAdapter{ private final IMiner miner; @Override diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/PredictionsHandler.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/PredictionsHandler.java index 14a2dd21..6a6d1107 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/PredictionsHandler.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/PredictionsHandler.java @@ -10,6 +10,7 @@ import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Prediction; import fr.raksrinana.channelpointsminer.miner.api.ws.data.request.topic.Topic; import fr.raksrinana.channelpointsminer.miner.event.impl.EventCreatedEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.EventUpdatedEvent; import fr.raksrinana.channelpointsminer.miner.event.impl.PredictionMadeEvent; import fr.raksrinana.channelpointsminer.miner.event.impl.PredictionResultEvent; import fr.raksrinana.channelpointsminer.miner.factory.TimeFactory; @@ -36,7 +37,7 @@ @RequiredArgsConstructor @Log4j2 -public class PredictionsHandler extends HandlerAdapter{ +public class PredictionsHandler extends PubSubMessageHandlerAdapter{ private static final int OFFSET = 5; private final IMiner miner; @@ -72,7 +73,12 @@ public void onEventUpdated(@NotNull Topic topic, @NotNull EventUpdated message){ var streamer = miner.getStreamerById(topic.getTarget()).orElse(null); var event = message.getData().getEvent(); try(var ignored = LogContext.with(miner).withStreamer(streamer).withEventId(event.getId())){ - var prediction = predictions.get(event.getId()); + + if(Objects.nonNull(streamer)){ + miner.onEvent(new EventUpdatedEvent(miner, TimeFactory.now(), streamer.getUsername(), event)); + } + + var prediction = predictions.get(event.getId()); if(Objects.isNull(prediction)){ log.debug("Event update on unknown prediction, creating it"); diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/HandlerAdapter.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/PubSubMessageHandlerAdapter.java similarity index 90% rename from miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/HandlerAdapter.java rename to miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/PubSubMessageHandlerAdapter.java index 0844ffe9..f5ede712 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/HandlerAdapter.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/PubSubMessageHandlerAdapter.java @@ -9,7 +9,7 @@ import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.EventCreated; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.EventUpdated; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.GlobalLastViewedContentUpdated; -import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IMessage; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IPubSubMessage; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.PointsEarned; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.PointsSpent; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.PredictionMade; @@ -24,6 +24,7 @@ import fr.raksrinana.channelpointsminer.miner.api.ws.data.request.topic.Topic; import fr.raksrinana.channelpointsminer.miner.util.ClassWalker; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; @@ -33,7 +34,7 @@ import java.util.concurrent.ConcurrentMap; @SuppressWarnings("unused") -public abstract class HandlerAdapter implements IMessageHandler{ +public abstract class PubSubMessageHandlerAdapter implements IPubSubMessageHandler{ private static final MethodHandles.Lookup lookup = MethodHandles.lookup(); private static final ConcurrentMap, MethodHandle> methods = new ConcurrentHashMap<>(); private static final Set> unresolved; @@ -79,12 +80,12 @@ public void onDeleteNotification(@NotNull Topic topic, @NotNull DeleteNotificati public void onUpdateSummary(@NotNull Topic topic, @NotNull UpdateSummary message){} @Override - public void handle(@NotNull Topic topic, @NotNull IMessage message){ - for(var clazz : ClassWalker.range(message.getClass(), IMessage.class)){ + public void handle(@NotNull Topic topic, @NotNull IPubSubMessage message){ + for(var clazz : ClassWalker.range(message.getClass(), IPubSubMessage.class)){ if(unresolved.contains(clazz)){ continue; } - var methodHandle = methods.computeIfAbsent(clazz, HandlerAdapter::findMethod); + var methodHandle = methods.computeIfAbsent(clazz, PubSubMessageHandlerAdapter::findMethod); if(methodHandle == null){ unresolved.add(clazz); continue; @@ -105,12 +106,13 @@ public void handle(@NotNull Topic topic, @NotNull IMessage message){ } } - private static MethodHandle findMethod(Class clazz){ + @Nullable + private static MethodHandle findMethod(@NotNull Class clazz){ var name = clazz.getSimpleName(); var type = MethodType.methodType(Void.TYPE, Topic.class, clazz); try{ name = "on" + name; - return lookup.findVirtual(HandlerAdapter.class, name, type); + return lookup.findVirtual(PubSubMessageHandlerAdapter.class, name, type); } catch(NoSuchMethodException | IllegalAccessException ignored){ } // this means this is probably a custom event! diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/StreamStartEndHandler.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/StreamStartEndHandler.java index c7ae92c7..929081c1 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/StreamStartEndHandler.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/handler/StreamStartEndHandler.java @@ -18,7 +18,7 @@ @RequiredArgsConstructor @Log4j2 -public class StreamStartEndHandler extends HandlerAdapter{ +public class StreamStartEndHandler extends PubSubMessageHandlerAdapter{ private final IMiner miner; @Override diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/log/DiscordEventListener.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/log/DiscordEventListener.java index a359af6e..0b895fb0 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/log/DiscordEventListener.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/log/DiscordEventListener.java @@ -1,33 +1,31 @@ package fr.raksrinana.channelpointsminer.miner.log; import fr.raksrinana.channelpointsminer.miner.api.discord.DiscordApi; -import fr.raksrinana.channelpointsminer.miner.event.IEvent; -import fr.raksrinana.channelpointsminer.miner.event.IEventListener; +import fr.raksrinana.channelpointsminer.miner.event.EventHandlerAdapter; import fr.raksrinana.channelpointsminer.miner.event.ILoggableEvent; import fr.raksrinana.channelpointsminer.miner.event.IStreamerEvent; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.jetbrains.annotations.NotNull; @Log4j2 @RequiredArgsConstructor -public class DiscordEventListener implements IEventListener{ +public class DiscordEventListener extends EventHandlerAdapter{ private final DiscordApi discordApi; private final boolean useEmbeds; @Override - public void onEvent(IEvent event){ - if(event instanceof ILoggableEvent loggableEvent){ - try(var context = LogContext.with(event.getMiner())){ - if(loggableEvent instanceof IStreamerEvent e){ - e.getStreamerUsername().ifPresent(context::withStreamer); - } - - if(useEmbeds){ - discordApi.sendMessage(loggableEvent.getAsWebhookEmbed()); - } - else{ - discordApi.sendMessage(loggableEvent.getAsWebhookMessage()); - } + public void onILoggableEvent(@NotNull ILoggableEvent event){ + try(var context = LogContext.with(event.getMiner())){ + if(event instanceof IStreamerEvent e){ + e.getStreamerUsername().ifPresent(context::withStreamer); + } + + if(useEmbeds){ + discordApi.sendMessage(event.getAsWebhookEmbed()); + } + else{ + discordApi.sendMessage(event.getAsWebhookMessage()); } } } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/log/LoggerEventListener.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/log/LoggerEventListener.java index 03621dad..b4207565 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/log/LoggerEventListener.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/log/LoggerEventListener.java @@ -1,24 +1,22 @@ package fr.raksrinana.channelpointsminer.miner.log; -import fr.raksrinana.channelpointsminer.miner.event.IEvent; -import fr.raksrinana.channelpointsminer.miner.event.IEventListener; +import fr.raksrinana.channelpointsminer.miner.event.EventHandlerAdapter; import fr.raksrinana.channelpointsminer.miner.event.ILoggableEvent; import fr.raksrinana.channelpointsminer.miner.event.IStreamerEvent; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.jetbrains.annotations.NotNull; @Log4j2 @RequiredArgsConstructor -public class LoggerEventListener implements IEventListener{ +public class LoggerEventListener extends EventHandlerAdapter{ @Override - public void onEvent(IEvent event){ - if(event instanceof ILoggableEvent loggableEvent){ - try(var context = LogContext.with(event.getMiner())){ - if(loggableEvent instanceof IStreamerEvent e){ - e.getStreamerUsername().ifPresent(context::withStreamer); - } - log.info(loggableEvent.getAsLog()); + public void onILoggableEvent(@NotNull ILoggableEvent event){ + try(var context = LogContext.with(event.getMiner())){ + if(event instanceof IStreamerEvent e){ + e.getStreamerUsername().ifPresent(context::withStreamer); } + log.info(event.getAsLog()); } } } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/miner/IMiner.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/miner/IMiner.java index 831db681..d4ba5d1b 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/miner/IMiner.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/miner/IMiner.java @@ -5,7 +5,8 @@ import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin; import fr.raksrinana.channelpointsminer.miner.api.twitch.TwitchApi; import fr.raksrinana.channelpointsminer.miner.api.ws.TwitchPubSubWebSocketPool; -import fr.raksrinana.channelpointsminer.miner.event.IEventListener; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; +import fr.raksrinana.channelpointsminer.miner.event.IEventHandler; import fr.raksrinana.channelpointsminer.miner.streamer.Streamer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -14,7 +15,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -public interface IMiner extends IEventListener{ +public interface IMiner extends IEventHandler{ @NotNull Optional getStreamerById(@NotNull String id); @@ -61,4 +62,7 @@ public interface IMiner extends IEventListener{ @NotNull TwitchPubSubWebSocketPool getPubSubWebSocketPool(); + + @NotNull + IDatabase getDatabase(); } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/miner/Miner.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/miner/Miner.java index 0fa6c2a0..49527036 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/miner/Miner.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/miner/Miner.java @@ -1,7 +1,7 @@ package fr.raksrinana.channelpointsminer.miner.miner; import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatClient; -import fr.raksrinana.channelpointsminer.miner.api.chat.TwitchChatFactory; +import fr.raksrinana.channelpointsminer.miner.api.chat.TwitchChatEventProducer; import fr.raksrinana.channelpointsminer.miner.api.gql.GQLApi; import fr.raksrinana.channelpointsminer.miner.api.passport.PassportApi; import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin; @@ -9,20 +9,22 @@ import fr.raksrinana.channelpointsminer.miner.api.twitch.TwitchApi; import fr.raksrinana.channelpointsminer.miner.api.ws.ITwitchPubSubMessageListener; import fr.raksrinana.channelpointsminer.miner.api.ws.TwitchPubSubWebSocketPool; -import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IMessage; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IPubSubMessage; import fr.raksrinana.channelpointsminer.miner.api.ws.data.request.topic.Topic; import fr.raksrinana.channelpointsminer.miner.api.ws.data.request.topic.TopicName; import fr.raksrinana.channelpointsminer.miner.api.ws.data.request.topic.Topics; import fr.raksrinana.channelpointsminer.miner.config.AccountConfiguration; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.event.IEvent; -import fr.raksrinana.channelpointsminer.miner.event.IEventListener; +import fr.raksrinana.channelpointsminer.miner.event.IEventHandler; import fr.raksrinana.channelpointsminer.miner.event.impl.StreamerAddedEvent; import fr.raksrinana.channelpointsminer.miner.event.impl.StreamerRemovedEvent; import fr.raksrinana.channelpointsminer.miner.factory.ApiFactory; import fr.raksrinana.channelpointsminer.miner.factory.MinerRunnableFactory; import fr.raksrinana.channelpointsminer.miner.factory.StreamerSettingsFactory; import fr.raksrinana.channelpointsminer.miner.factory.TimeFactory; -import fr.raksrinana.channelpointsminer.miner.handler.IMessageHandler; +import fr.raksrinana.channelpointsminer.miner.factory.TwitchChatFactory; +import fr.raksrinana.channelpointsminer.miner.handler.IPubSubMessageHandler; import fr.raksrinana.channelpointsminer.miner.log.LogContext; import fr.raksrinana.channelpointsminer.miner.runnable.SyncInventory; import fr.raksrinana.channelpointsminer.miner.runnable.UpdateStreamInfo; @@ -63,17 +65,19 @@ public class Miner implements AutoCloseable, IMiner, ITwitchPubSubMessageListene private final TwitchPubSubWebSocketPool pubSubWebSocketPool; private final ScheduledExecutorService scheduledExecutor; private final ExecutorService handlerExecutor; + @Getter + private final IDatabase database; private final StreamerSettingsFactory streamerSettingsFactory; @Getter(value = AccessLevel.PUBLIC, onMethod_ = { @TestOnly, @VisibleForTesting }) - private final Collection messageHandlers; + private final Collection pubSubMessageHandlers; @Getter(value = AccessLevel.PUBLIC, onMethod_ = { @TestOnly, @VisibleForTesting }) - private final Collection eventListeners; + private final Collection eventHandlers; @Getter private final MinerData minerData; @@ -94,17 +98,19 @@ public Miner(@NotNull AccountConfiguration accountConfiguration, @NotNull StreamerSettingsFactory streamerSettingsFactory, @NotNull TwitchPubSubWebSocketPool pubSubWebSocketPool, @NotNull ScheduledExecutorService scheduledExecutor, - @NotNull ExecutorService handlerExecutor){ + @NotNull ExecutorService handlerExecutor, + @NotNull IDatabase database){ this.accountConfiguration = accountConfiguration; this.passportApi = passportApi; this.streamerSettingsFactory = streamerSettingsFactory; this.pubSubWebSocketPool = pubSubWebSocketPool; this.scheduledExecutor = scheduledExecutor; this.handlerExecutor = handlerExecutor; + this.database = database; streamers = new ConcurrentHashMap<>(); - messageHandlers = new ConcurrentLinkedQueue<>(); - eventListeners = new ConcurrentLinkedQueue<>(); + pubSubMessageHandlers = new ConcurrentLinkedQueue<>(); + eventHandlers = new ConcurrentLinkedQueue<>(); minerData = new MinerData(); } @@ -145,10 +151,14 @@ public void start(){ */ private void login(){ try{ + var analyticsConfiguration = accountConfiguration.getAnalytics(); + var listenMessages = analyticsConfiguration.isEnabled() && analyticsConfiguration.isRecordChatsPredictions(); + twitchLogin = passportApi.login(); gqlApi = ApiFactory.createGqlApi(twitchLogin); twitchApi = ApiFactory.createTwitchApi(); - chatClient = TwitchChatFactory.createChat(accountConfiguration.getChatMode(), twitchLogin); + chatClient = TwitchChatFactory.createChat(this, accountConfiguration.getChatMode(), listenMessages); + chatClient.addChatMessageListener(new TwitchChatEventProducer(this)); } catch(CaptchaSolveRequired e){ throw new IllegalStateException("A captcha solve is required, please log in through your browser and solve it"); @@ -301,7 +311,7 @@ public void onEvent(IEvent event){ var values = ThreadContext.getImmutableContext(); var messages = ThreadContext.getImmutableStack().asList(); - eventListeners.forEach(listener -> handlerExecutor.submit(() -> { + eventHandlers.forEach(listener -> handlerExecutor.submit(() -> { try(var ignored = LogContext.restore(values, messages)){ listener.onEvent(event); } @@ -313,23 +323,23 @@ private void removeTopic(@NotNull TopicName name, @NotNull String target){ } @Override - public void onTwitchMessage(@NotNull Topic topic, @NotNull IMessage message){ + public void onTwitchMessage(@NotNull Topic topic, @NotNull IPubSubMessage message){ var values = ThreadContext.getImmutableContext(); var messages = ThreadContext.getImmutableStack().asList(); - messageHandlers.forEach(handler -> handlerExecutor.submit(() -> { + pubSubMessageHandlers.forEach(handler -> handlerExecutor.submit(() -> { try(var ignored = LogContext.restore(values, messages)){ handler.handle(topic, message); } })); } - public void addHandler(@NotNull IMessageHandler handler){ - messageHandlers.add(handler); + public void addPubSubHandler(@NotNull IPubSubMessageHandler handler){ + pubSubMessageHandlers.add(handler); } - public void addEventListener(@NotNull IEventListener listener){ - eventListeners.add(listener); + public void addEventHandler(@NotNull IEventHandler handler){ + eventHandlers.add(handler); } @Override @@ -340,7 +350,7 @@ public void close(){ if(!Objects.isNull(chatClient)){ chatClient.close(); } - for(var listener : eventListeners){ + for(var listener : eventHandlers){ listener.close(); } } @@ -351,4 +361,4 @@ public void close(){ protected Map getStreamerMap(){ return streamers; } -} +} \ No newline at end of file diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/BetPlacer.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/BetPlacer.java index 02e9c4ea..8d407b81 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/BetPlacer.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/BetPlacer.java @@ -34,7 +34,7 @@ public void placeBet(@NotNull BettingPrediction bettingPrediction){ } var outcomePicker = bettingPrediction.getStreamer().getSettings().getPredictions().getOutcomePicker(); - var outcome = outcomePicker.chooseOutcome(bettingPrediction); + var outcome = outcomePicker.chooseOutcome(bettingPrediction, miner.getDatabase()); var amountCalculator = bettingPrediction.getStreamer().getSettings().getPredictions().getAmountCalculator(); var amount = amountCalculator.calculateAmount(bettingPrediction, outcome); diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/exception/BetPlacementException.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/exception/BetPlacementException.java index 5f11502d..77219c02 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/exception/BetPlacementException.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/exception/BetPlacementException.java @@ -1,9 +1,14 @@ package fr.raksrinana.channelpointsminer.miner.prediction.bet.exception; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public class BetPlacementException extends Exception{ public BetPlacementException(@NotNull String message){ super(message); } + + public BetPlacementException(@NotNull String message, @Nullable Throwable e){ + super(message, e); + } } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/BiggestPredictorOutcomePicker.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/BiggestPredictorOutcomePicker.java index c43c4913..7ed3a68c 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/BiggestPredictorOutcomePicker.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/BiggestPredictorOutcomePicker.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonTypeName; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Predictor; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; import lombok.AllArgsConstructor; @@ -23,7 +24,7 @@ public class BiggestPredictorOutcomePicker implements IOutcomePicker{ @Override @NotNull - public Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction) throws BetPlacementException{ + public Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction, @NotNull IDatabase database) throws BetPlacementException{ return bettingPrediction.getEvent().getOutcomes().stream() .max(this::compare) .orElseThrow(() -> new BetPlacementException("Couldn't get outcome with biggest predictor points")); diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/IOutcomePicker.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/IOutcomePicker.java index 89b1bd50..49a4bf4e 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/IOutcomePicker.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/IOutcomePicker.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; import org.jetbrains.annotations.NotNull; @@ -16,8 +17,9 @@ @JsonSubTypes.Type(value = LeastPointsOutcomePicker.class, name = "leastPoints"), @JsonSubTypes.Type(value = SmartOutcomePicker.class, name = "smart"), @JsonSubTypes.Type(value = BiggestPredictorOutcomePicker.class, name = "biggestPredictor"), + @JsonSubTypes.Type(value = MostTrustedPicker.class, name = "mostTrusted"), }) public interface IOutcomePicker{ @NotNull - Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction) throws BetPlacementException; + Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction, @NotNull IDatabase database) throws BetPlacementException; } diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastPointsOutcomePicker.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastPointsOutcomePicker.java index 5b85c70d..52737628 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastPointsOutcomePicker.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastPointsOutcomePicker.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonTypeName; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; import lombok.AllArgsConstructor; @@ -23,7 +24,7 @@ public class LeastPointsOutcomePicker implements IOutcomePicker{ @Override @NotNull - public Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction) throws BetPlacementException{ + public Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction, @NotNull IDatabase database) throws BetPlacementException{ return bettingPrediction.getEvent().getOutcomes().stream() .min(Comparator.comparingLong(Outcome::getTotalPoints)) .orElseThrow(() -> new BetPlacementException("Couldn't get outcome with least points")); diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastUsersOutcomePicker.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastUsersOutcomePicker.java index 2cc1fa3d..037242e8 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastUsersOutcomePicker.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastUsersOutcomePicker.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonTypeName; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; import lombok.AllArgsConstructor; @@ -23,7 +24,7 @@ public class LeastUsersOutcomePicker implements IOutcomePicker{ @Override @NotNull - public Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction) throws BetPlacementException{ + public Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction, @NotNull IDatabase database) throws BetPlacementException{ return bettingPrediction.getEvent().getOutcomes().stream() .min(Comparator.comparingInt(Outcome::getTotalUsers)) .orElseThrow(() -> new BetPlacementException("Couldn't get outcome with least users")); diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostPointsOutcomePicker.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostPointsOutcomePicker.java index 6bf436b7..5b6565d0 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostPointsOutcomePicker.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostPointsOutcomePicker.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonTypeName; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; import lombok.AllArgsConstructor; @@ -23,7 +24,7 @@ public class MostPointsOutcomePicker implements IOutcomePicker{ @Override @NotNull - public Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction) throws BetPlacementException{ + public Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction, @NotNull IDatabase database) throws BetPlacementException{ return bettingPrediction.getEvent().getOutcomes().stream() .max(Comparator.comparingLong(Outcome::getTotalPoints)) .orElseThrow(() -> new BetPlacementException("Couldn't get outcome with most points")); diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostTrustedPicker.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostTrustedPicker.java new file mode 100644 index 00000000..c3f55d62 --- /dev/null +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostTrustedPicker.java @@ -0,0 +1,86 @@ +package fr.raksrinana.channelpointsminer.miner.prediction.bet.outcome; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; +import fr.raksrinana.channelpointsminer.miner.database.NoOpDatabase; +import fr.raksrinana.channelpointsminer.miner.database.model.prediction.OutcomeStatistic; +import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; +import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.extern.log4j.Log4j2; +import org.jetbrains.annotations.NotNull; +import java.util.Comparator; + +@JsonTypeName("mostTrusted") +@Getter +@EqualsAndHashCode +@ToString +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Log4j2 +public class MostTrustedPicker implements IOutcomePicker{ + + @Builder.Default + private int minTotalBetsPlacedByUser = 5; + @Builder.Default + private int minTotalBetsPlacedOnPrediction = 10; + @Builder.Default + private int minTotalBetsPlacedOnOutcome = 5; + + @Override + @NotNull + public Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction, @NotNull IDatabase database) throws BetPlacementException{ + + try{ + if(database instanceof NoOpDatabase){ + throw new BetPlacementException("A database needs to be configured for this outcome picker to work"); + } + + var outcomes = bettingPrediction.getEvent().getOutcomes(); + var title = bettingPrediction.getEvent().getTitle(); + + var outcomeStatistics = database.getOutcomeStatisticsForChannel(bettingPrediction.getEvent().getChannelId(), minTotalBetsPlacedByUser); + + var mostTrusted = outcomeStatistics.stream() + .max(Comparator.comparingDouble(OutcomeStatistic::getAverageReturnOnInvestment)) + .orElseThrow(() -> new BetPlacementException("No outcome statistics found. Maybe not enough data gathered yet.")); + + for(var outcomeStats : outcomeStatistics){ + log.info("Outcome stats for '{}': {}", outcomeStats.getBadge(), outcomeStats.toString()); + } + + int totalBetsPlaced = outcomeStatistics.stream().mapToInt(OutcomeStatistic::getUserCnt).sum(); + if(totalBetsPlaced < minTotalBetsPlacedOnPrediction){ + throw new BetPlacementException("Not enough bets placed for prediction %s. Minimum is %d. Was %d".formatted(title, minTotalBetsPlacedOnPrediction, totalBetsPlaced)); + } + + var chosenOutcome = outcomes.stream() + .filter(o -> o.getBadge().getVersion().equalsIgnoreCase(mostTrusted.getBadge())) + .findAny() + .orElseThrow(() -> new BetPlacementException("Outcome badge not found: %s".formatted(mostTrusted.getBadge()))); + + if(mostTrusted.getUserCnt() < minTotalBetsPlacedOnOutcome){ + throw new BetPlacementException( + "Not enough bets placed on chosen outcome: '%s'. Minimum is %d. Was %d".formatted(chosenOutcome.getTitle(), minTotalBetsPlacedOnOutcome, mostTrusted.getUserCnt())); + } + + log.info("Prediction: '{}'. Most trusted outcome (highest average return of investment of other bettors): Title: '{}', Badge: {}.", + title, chosenOutcome.getTitle(), chosenOutcome.getBadge().getVersion()); + + return chosenOutcome; + } + catch(BetPlacementException e){ + throw e; + } + catch(Exception e){ + throw new BetPlacementException("Bet placement failed", e); + } + } +} \ No newline at end of file diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostUsersOutcomePicker.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostUsersOutcomePicker.java index 42177d07..18252748 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostUsersOutcomePicker.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostUsersOutcomePicker.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonTypeName; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; import lombok.AllArgsConstructor; @@ -23,7 +24,7 @@ public class MostUsersOutcomePicker implements IOutcomePicker{ @Override @NotNull - public Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction) throws BetPlacementException{ + public Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction, @NotNull IDatabase database) throws BetPlacementException{ return bettingPrediction.getEvent().getOutcomes().stream() .max(Comparator.comparingInt(Outcome::getTotalUsers)) .orElseThrow(() -> new BetPlacementException("Couldn't get outcome with most users")); diff --git a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/SmartOutcomePicker.java b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/SmartOutcomePicker.java index 03c5dafe..5528a5f6 100644 --- a/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/SmartOutcomePicker.java +++ b/miner/src/main/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/SmartOutcomePicker.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonTypeName; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.NotEnoughUsersBetPlacementException; @@ -30,7 +31,7 @@ public class SmartOutcomePicker implements IOutcomePicker{ @Override @NotNull - public Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction) throws BetPlacementException{ + public Outcome chooseOutcome(@NotNull BettingPrediction bettingPrediction, @NotNull IDatabase database) throws BetPlacementException{ var totalUsers = (double) bettingPrediction.getEvent().getOutcomes().stream().mapToInt(Outcome::getTotalUsers).sum(); if(Double.compare(0D, totalUsers) == 0){ diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/TwitchChatEventProducerTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/TwitchChatEventProducerTest.java new file mode 100644 index 00000000..1985f0d1 --- /dev/null +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/TwitchChatEventProducerTest.java @@ -0,0 +1,55 @@ +package fr.raksrinana.channelpointsminer.miner.api.chat; + +import fr.raksrinana.channelpointsminer.miner.event.impl.ChatMessageEvent; +import fr.raksrinana.channelpointsminer.miner.factory.TimeFactory; +import fr.raksrinana.channelpointsminer.miner.miner.IMiner; +import fr.raksrinana.channelpointsminer.miner.tests.ParallelizableTest; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import java.time.Instant; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; + +@ParallelizableTest +@ExtendWith(MockitoExtension.class) +class TwitchChatEventProducerTest{ + private static final Instant NOW = Instant.parse("2020-05-04T20:41:14.000Z"); + + private final static String STREAMER_NAME = "channel"; + private final static String ACTOR = "username"; + private final static String MESSAGE = "message"; + private final static String BADGE = "badges=predictions/color,sub"; + + @InjectMocks + private TwitchChatEventProducer tested; + + @Mock + private IMiner miner; + + @Test + void onMessage(){ + try(var factory = mockStatic(TimeFactory.class)){ + factory.when(TimeFactory::now).thenReturn(NOW); + + assertDoesNotThrow(() -> tested.onChatMessage(STREAMER_NAME, ACTOR, MESSAGE)); + + var expectedEvent = new ChatMessageEvent(miner, NOW, STREAMER_NAME, ACTOR, MESSAGE, ""); + verify(miner).onEvent(expectedEvent); + } + } + + @Test + void onMessageWithBadge(){ + try(var factory = mockStatic(TimeFactory.class)){ + factory.when(TimeFactory::now).thenReturn(NOW); + assertDoesNotThrow(() -> tested.onChatMessage(STREAMER_NAME, ACTOR, MESSAGE, BADGE)); + + var expectedEvent = new ChatMessageEvent(miner, NOW, STREAMER_NAME, ACTOR, MESSAGE, BADGE); + verify(miner).onEvent(expectedEvent); + } + } +} \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcChatClientTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcChatClientTest.java index a7d4ffd6..7884cf2d 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcChatClientTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcChatClientTest.java @@ -1,11 +1,14 @@ package fr.raksrinana.channelpointsminer.miner.api.chat.irc; +import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatMessageListener; import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin; import fr.raksrinana.channelpointsminer.miner.tests.ParallelizableTest; import org.kitteh.irc.client.library.Client; +import org.kitteh.irc.client.library.command.CapabilityRequestCommand; +import org.kitteh.irc.client.library.defaults.element.messagetag.DefaultMessageTagLabel; import org.kitteh.irc.client.library.element.Channel; import org.kitteh.irc.client.library.feature.EventManager; -import org.mockito.InjectMocks; +import org.kitteh.irc.client.library.feature.MessageTagManager; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.junit.jupiter.api.BeforeEach; @@ -29,22 +32,38 @@ class TwitchIrcChatClientTest{ private static final String ACCESS_TOKEN = "password"; private static final String STREAMER = "streamer"; private static final String STREAMER_CHANNEL = "#streamer"; + private static final String TAG_NAME = "tag"; + private static final String CAPABILITY_NAME = "cap"; - @InjectMocks private TwitchIrcChatClient tested; @Mock private TwitchLogin twitchLogin; @Mock + private TwitchIrcConnectionHandler twitchIrcConnectionHandler; + @Mock + private TwitchIrcMessageHandler twitchIrcMessageHandler; + @Mock private Client client; @Mock private EventManager eventManager; @Mock - private TwitchIrcEventListener listener; + private MessageTagManager tagManager; + @Mock + private Client.Commands commands; + @Mock + private CapabilityRequestCommand capabilityRequestCommand; + @Mock + private ITwitchChatMessageListener chatMessageListener; @BeforeEach void setUp(){ + tested = new TwitchIrcChatClient(twitchLogin, false); + lenient().when(client.getEventManager()).thenReturn(eventManager); + lenient().when(client.commands()).thenReturn(commands); + lenient().when(client.getMessageTagManager()).thenReturn(tagManager); + lenient().when(commands.capabilityRequest()).thenReturn(capabilityRequestCommand); lenient().when(twitchLogin.getUsername()).thenReturn(USERNAME); lenient().when(twitchLogin.getAccessToken()).thenReturn(ACCESS_TOKEN); @@ -54,22 +73,70 @@ void setUp(){ void joinChannelCreatesClient(){ try(var factory = mockStatic(TwitchIrcFactory.class)){ factory.when(() -> TwitchIrcFactory.createIrcClient(twitchLogin)).thenReturn(client); - factory.when(() -> TwitchIrcFactory.createIrcListener(USERNAME)).thenReturn(listener); + factory.when(() -> TwitchIrcFactory.createIrcConnectionHandler(USERNAME)).thenReturn(twitchIrcConnectionHandler); assertDoesNotThrow(() -> tested.join(STREAMER)); factory.verify(() -> TwitchIrcFactory.createIrcClient(twitchLogin)); + verify(client).connect(); - verify(eventManager).registerEventListener(listener); + verify(eventManager).registerEventListener(twitchIrcConnectionHandler); + verify(capabilityRequestCommand, never()).enable(any()); + verify(tagManager, never()).registerTagCreator(any(), any(), any()); verify(client).addChannel(STREAMER_CHANNEL); } } + @Test + void joinChannelCreatesClientWithMessageListening(){ + tested = new TwitchIrcChatClient(twitchLogin, true); + + try(var factory = mockStatic(TwitchIrcFactory.class)){ + factory.when(() -> TwitchIrcFactory.createIrcClient(twitchLogin)).thenReturn(client); + factory.when(() -> TwitchIrcFactory.createIrcConnectionHandler(USERNAME)).thenReturn(twitchIrcConnectionHandler); + factory.when(() -> TwitchIrcFactory.createIrcMessageHandler(USERNAME)).thenReturn(twitchIrcMessageHandler); + + tested.addChatMessageListener(chatMessageListener); + + assertDoesNotThrow(() -> tested.join(STREAMER)); + + factory.verify(() -> TwitchIrcFactory.createIrcClient(twitchLogin)); + verify(client).connect(); + verify(eventManager).registerEventListener(twitchIrcConnectionHandler); + verify(eventManager).registerEventListener(twitchIrcMessageHandler); + + verify(capabilityRequestCommand).enable("twitch.tv/tags"); + + verify(tagManager).registerTagCreator("twitch.tv/tags", "emote-sets", DefaultMessageTagLabel.FUNCTION); + + verify(client).addChannel(STREAMER_CHANNEL); + verify(twitchIrcMessageHandler).addListener(chatMessageListener); + } + } + + @Test + void addMessageListenerPropagatesListener(){ + tested = new TwitchIrcChatClient(twitchLogin, true); + + try(var factory = mockStatic(TwitchIrcFactory.class)){ + factory.when(() -> TwitchIrcFactory.createIrcClient(twitchLogin)).thenReturn(client); + factory.when(() -> TwitchIrcFactory.createIrcConnectionHandler(USERNAME)).thenReturn(twitchIrcConnectionHandler); + factory.when(() -> TwitchIrcFactory.createIrcMessageHandler(USERNAME)).thenReturn(twitchIrcMessageHandler); + + assertDoesNotThrow(() -> tested.join(STREAMER)); + + verify(twitchIrcMessageHandler, never()).addListener(chatMessageListener); + + tested.addChatMessageListener(chatMessageListener); + verify(twitchIrcMessageHandler).addListener(chatMessageListener); + } + } + @Test void joinChannelCreatesClientOnlyOnce(){ try(var factory = mockStatic(TwitchIrcFactory.class)){ factory.when(() -> TwitchIrcFactory.createIrcClient(twitchLogin)).thenReturn(client); - factory.when(() -> TwitchIrcFactory.createIrcListener(USERNAME)).thenReturn(listener); + factory.when(() -> TwitchIrcFactory.createIrcConnectionHandler(USERNAME)).thenReturn(twitchIrcConnectionHandler); assertDoesNotThrow(() -> tested.join(STREAMER)); assertDoesNotThrow(() -> tested.join(STREAMER)); @@ -84,7 +151,7 @@ void joinChannelCreatesClientOnlyOnce(){ void joinChannelAlreadyJoined(){ try(var factory = mockStatic(TwitchIrcFactory.class)){ factory.when(() -> TwitchIrcFactory.createIrcClient(twitchLogin)).thenReturn(client); - factory.when(() -> TwitchIrcFactory.createIrcListener(USERNAME)).thenReturn(listener); + factory.when(() -> TwitchIrcFactory.createIrcConnectionHandler(USERNAME)).thenReturn(twitchIrcConnectionHandler); var channel = mock(Channel.class); when(client.getChannel(STREAMER_CHANNEL)).thenReturn(Optional.of(channel)); @@ -93,7 +160,9 @@ void joinChannelAlreadyJoined(){ factory.verify(() -> TwitchIrcFactory.createIrcClient(twitchLogin)); verify(client).connect(); - verify(eventManager).registerEventListener(listener); + + verify(eventManager).registerEventListener(twitchIrcConnectionHandler); + verify(client, never()).addChannel(any()); } } @@ -107,7 +176,7 @@ void leaveWhenNotOpened(){ void leaveChannel(){ try(var factory = mockStatic(TwitchIrcFactory.class)){ factory.when(() -> TwitchIrcFactory.createIrcClient(twitchLogin)).thenReturn(client); - factory.when(() -> TwitchIrcFactory.createIrcListener(USERNAME)).thenReturn(listener); + factory.when(() -> TwitchIrcFactory.createIrcConnectionHandler(USERNAME)).thenReturn(twitchIrcConnectionHandler); var channel = mock(Channel.class); when(client.getChannel(STREAMER_CHANNEL)).thenReturn(Optional.of(channel)); @@ -124,7 +193,7 @@ void leaveChannel(){ void leaveNotJoinedChannel(){ try(var factory = mockStatic(TwitchIrcFactory.class)){ factory.when(() -> TwitchIrcFactory.createIrcClient(twitchLogin)).thenReturn(client); - factory.when(() -> TwitchIrcFactory.createIrcListener(USERNAME)).thenReturn(listener); + factory.when(() -> TwitchIrcFactory.createIrcConnectionHandler(USERNAME)).thenReturn(twitchIrcConnectionHandler); when(client.getChannel(STREAMER_CHANNEL)).thenReturn(Optional.empty()); @@ -140,7 +209,7 @@ void leaveNotJoinedChannel(){ void close(){ try(var factory = mockStatic(TwitchIrcFactory.class)){ factory.when(() -> TwitchIrcFactory.createIrcClient(twitchLogin)).thenReturn(client); - factory.when(() -> TwitchIrcFactory.createIrcListener(USERNAME)).thenReturn(listener); + factory.when(() -> TwitchIrcFactory.createIrcConnectionHandler(USERNAME)).thenReturn(twitchIrcConnectionHandler); assertDoesNotThrow(() -> tested.join(STREAMER)); assertDoesNotThrow(() -> tested.close()); diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcEventListenerTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcConnectionHandlerTest.java similarity index 91% rename from miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcEventListenerTest.java rename to miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcConnectionHandlerTest.java index db531e7f..1efaceeb 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcEventListenerTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcConnectionHandlerTest.java @@ -14,10 +14,10 @@ @ParallelizableTest @ExtendWith(MockitoExtension.class) -class TwitchIrcEventListenerTest{ +class TwitchIrcConnectionHandlerTest{ private static final String USERNAME = "username"; - private final TwitchIrcEventListener tested = new TwitchIrcEventListener(USERNAME); + private final TwitchIrcConnectionHandler tested = new TwitchIrcConnectionHandler(USERNAME); @Mock private ClientConnectionClosedEvent clientConnectionClosedEvent; diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcFactoryTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcFactoryTest.java index 8f911467..932a5969 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcFactoryTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcFactoryTest.java @@ -25,7 +25,14 @@ void createFromTwitchLogin(){ } @Test - void createListener(){ - assertThat(TwitchIrcFactory.createIrcListener("username")).isNotNull().isInstanceOf(TwitchIrcEventListener.class); + void createIrcConnectionHandler(){ + assertThat(TwitchIrcFactory.createIrcConnectionHandler("username")).isNotNull() + .isInstanceOf(TwitchIrcConnectionHandler.class); + } + + @Test + void createIrcMessageHandler(){ + assertThat(TwitchIrcFactory.createIrcMessageHandler("username")).isNotNull() + .isInstanceOf(TwitchIrcMessageHandler.class); } } \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcMessageHandlerTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcMessageHandlerTest.java new file mode 100644 index 00000000..6f018d88 --- /dev/null +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/irc/TwitchIrcMessageHandlerTest.java @@ -0,0 +1,72 @@ +package fr.raksrinana.channelpointsminer.miner.api.chat.irc; + +import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatMessageListener; +import fr.raksrinana.channelpointsminer.miner.tests.ParallelizableTest; +import org.kitteh.irc.client.library.element.Channel; +import org.kitteh.irc.client.library.element.MessageTag; +import org.kitteh.irc.client.library.element.User; +import org.kitteh.irc.client.library.event.channel.ChannelMessageEvent; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import java.util.Optional; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ParallelizableTest +@ExtendWith(MockitoExtension.class) +class TwitchIrcMessageHandlerTest{ + private static final String USERNAME = "username"; + private static final String STREAMER = "streamer"; + private static final String STREAMER_CHANNEL = "#streamer"; + private static final String BADGE_INFO = "badge-info"; + private static final String MESSAGE = "message"; + + private TwitchIrcMessageHandler tested; + + @Mock + private ITwitchChatMessageListener chatMessageListener; + @Mock + private ChannelMessageEvent channelMessageEvent; + @Mock + private Channel chatChannel; + @Mock + private User chatUser; + @Mock + private MessageTag messageTag; + + @BeforeEach + void setUp(){ + tested = new TwitchIrcMessageHandler(USERNAME); + tested.addListener(chatMessageListener); + + lenient().when(channelMessageEvent.getChannel()).thenReturn(chatChannel); + lenient().when(chatChannel.getName()).thenReturn(STREAMER_CHANNEL); + lenient().when(channelMessageEvent.getActor()).thenReturn(chatUser); + lenient().when(chatUser.getMessagingName()).thenReturn(USERNAME); + lenient().when(channelMessageEvent.getMessage()).thenReturn(MESSAGE); + } + + @Test + void messageListenersCalledWithBadge(){ + when(channelMessageEvent.getTag("badges")).thenReturn(Optional.of(messageTag)); + when(messageTag.getAsString()).thenReturn(BADGE_INFO); + + assertDoesNotThrow(() -> tested.onMessageEvent(channelMessageEvent)); + + verify(chatMessageListener).onChatMessage(STREAMER, USERNAME, MESSAGE, BADGE_INFO); + } + + @Test + void messageListenersCalledWithoutBadge(){ + when(channelMessageEvent.getTag("badges")).thenReturn(Optional.empty()); + + assertDoesNotThrow(() -> tested.onMessageEvent(channelMessageEvent)); + + verify(chatMessageListener).onChatMessage(STREAMER, USERNAME, MESSAGE); + } +} \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketClientLeaveJoinTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketClientLeaveJoinTest.java index f560560c..08db1a14 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketClientLeaveJoinTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketClientLeaveJoinTest.java @@ -22,7 +22,7 @@ class TwitchChatWebSocketClientLeaveJoinTest{ private TwitchChatWebSocketClient tested; @Mock - private ITwitchChatWebSocketListener listener; + private ITwitchChatWebSocketClosedListener listener; @Mock private TwitchLogin twitchLogin; @@ -77,9 +77,9 @@ void setUp(WebsocketMockServer server) throws InterruptedException{ when(twitchLogin.getAccessToken()).thenReturn(ACCESS_TOKEN); var uri = URI.create("ws://127.0.0.1:" + server.getPort()); - tested = new TwitchChatWebSocketClient(uri, twitchLogin); + tested = new TwitchChatWebSocketClient(uri, twitchLogin, false); tested.setReuseAddr(true); - tested.addListener(listener); + tested.addWebSocketClosedListener(listener); tested.connectBlocking(); server.awaitMessage(3); server.reset(); diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketClientTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketClientTest.java index cd0456ed..f8d5a05c 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketClientTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketClientTest.java @@ -1,5 +1,6 @@ package fr.raksrinana.channelpointsminer.miner.api.chat.ws; +import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatMessageListener; import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin; import fr.raksrinana.channelpointsminer.miner.tests.WebsocketMockServer; import fr.raksrinana.channelpointsminer.miner.tests.WebsocketMockServerExtension; @@ -22,21 +23,41 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @ExtendWith(WebsocketMockServerExtension.class) class TwitchChatWebSocketClientTest{ - private static final int MESSAGE_TIMEOUT = 15000; private static final String USERNAME = "USERNAME"; private static final String ACCESS_TOKEN = "token"; + private static final String STREAMER = "streamer"; + private static final String STREAMER_CHANNEL = "#streamer"; + private static final String BADGE_INFO = "badge/info"; + private static final String MESSAGE = "message"; + + private static final String MESSAGE_PAYLOAD = + "@badge-info=;badges=" + BADGE_INFO + ";client-nonce=0;color=0;display-name=" + USERNAME + ";" + + "emotes=0;first-msg=0;flags=;id=id;mod=0;returning-chatter=0;room-id=room;subscriber=0;tmi-sent-ts=0;turbo=0;" + + "user-id=userid;user-type= :usertype.tmi.twitch.tv PRIVMSG " + + STREAMER_CHANNEL + " :" + MESSAGE; private TwitchChatWebSocketClient tested; @Mock - private ITwitchChatWebSocketListener listener; + private ITwitchChatWebSocketClosedListener listener; @Mock private TwitchLogin twitchLogin; + @Mock + private ITwitchChatMessageListener chatMessageListener; + + @Test + void onMessageChatMessageWithoutChatMonitored(){ + tested.onMessage(MESSAGE_PAYLOAD); + + verify(chatMessageListener, never()).onChatMessage(any(), any(), any()); + verify(chatMessageListener, never()).onChatMessage(any(), any(), any(), any()); + } @AfterEach void tearDown(WebsocketMockServer server){ @@ -54,6 +75,8 @@ void connect(WebsocketMockServer server) throws InterruptedException{ "PASS oauth:%s".formatted(ACCESS_TOKEN), "NICK %s".formatted(USERNAME.toLowerCase()) ); + + assertThat(tested.getUuid()).isNotNull(); } @Test @@ -123,14 +146,29 @@ void sendPing(WebsocketMockServer server) throws InterruptedException{ assertThat(server.getReceivedMessages()).contains("PING"); } + @Test + void onMessageChatMessageListenerCalled(){ + tested.setListenMessages(true); + tested.addChatMessageListener(chatMessageListener); + + tested.onMessage(MESSAGE_PAYLOAD); + + verify(chatMessageListener).onChatMessage(STREAMER, USERNAME, MESSAGE, BADGE_INFO); + } + + @Test + void onMessageException(){ + assertDoesNotThrow(() -> tested.onMessage((String) null)); //This is theoretically impossible + } + @BeforeEach - void setUp(WebsocketMockServer server) throws InterruptedException{ + void setUp(WebsocketMockServer server){ lenient().when(twitchLogin.getUsername()).thenReturn(USERNAME); lenient().when(twitchLogin.getAccessToken()).thenReturn(ACCESS_TOKEN); var uri = URI.create("ws://127.0.0.1:" + server.getPort()); - tested = new TwitchChatWebSocketClient(uri, twitchLogin); + tested = new TwitchChatWebSocketClient(uri, twitchLogin, false); tested.setReuseAddr(true); - tested.addListener(listener); + tested.addWebSocketClosedListener(listener); } } \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketPoolTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketPoolTest.java index 5f76d70c..e4f2dcf8 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketPoolTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/ws/TwitchChatWebSocketPoolTest.java @@ -1,5 +1,6 @@ package fr.raksrinana.channelpointsminer.miner.api.chat.ws; +import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatMessageListener; import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin; import fr.raksrinana.channelpointsminer.miner.factory.TimeFactory; import fr.raksrinana.channelpointsminer.miner.factory.TwitchWebSocketClientFactory; @@ -39,25 +40,43 @@ class TwitchChatWebSocketPoolTest{ @Mock private TwitchLogin twitchLogin; - + @Mock + private ITwitchChatMessageListener chatMessageListener; @Mock private TwitchChatWebSocketClient client; @BeforeEach void setUp(){ - tested = new TwitchChatWebSocketPool(50, twitchLogin); + tested = new TwitchChatWebSocketPool(50, twitchLogin, false); } @Test void addChannelCreatesNewClient() throws InterruptedException{ try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ - twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin)).thenReturn(client); + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); assertDoesNotThrow(() -> tested.join(STREAMER)); assertThat(tested.getClientCount()).isEqualTo(1); - verify(client).addListener(tested); + verify(client).addWebSocketClosedListener(tested); + verify(client).connectBlocking(); + verify(client).join(STREAMER_LOWER); + } + } + + @Test + void addChannelCreatesNewClientWithMessageListening() throws InterruptedException{ + tested = new TwitchChatWebSocketPool(50, twitchLogin, true); + + try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, true)).thenReturn(client); + + assertDoesNotThrow(() -> tested.join(STREAMER)); + + assertThat(tested.getClientCount()).isEqualTo(1); + + verify(client).addWebSocketClosedListener(tested); verify(client).connectBlocking(); verify(client).join(STREAMER_LOWER); } @@ -66,7 +85,7 @@ void addChannelCreatesNewClient() throws InterruptedException{ @Test void addNewChannelToExistingClient() throws InterruptedException{ try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ - twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin)).thenReturn(client); + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); when(client.isChannelJoined(STREAMER_LOWER)).thenReturn(false); @@ -75,16 +94,52 @@ void addNewChannelToExistingClient() throws InterruptedException{ assertThat(tested.getClientCount()).isEqualTo(1); - verify(client).addListener(tested); + verify(client).addWebSocketClosedListener(tested); verify(client).connectBlocking(); verify(client, times(2)).join(STREAMER_LOWER); } } + @Test + void addMessageListenerToNewClient() throws InterruptedException{ + try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); + + tested.addChatMessageListener(chatMessageListener); + + assertDoesNotThrow(() -> tested.join(STREAMER)); + + assertThat(tested.getClientCount()).isEqualTo(1); + + verify(client).addWebSocketClosedListener(tested); + verify(client).connectBlocking(); + verify(client).join(STREAMER_LOWER); + verify(client).addChatMessageListener(chatMessageListener); + } + } + + @Test + void addMessageListenerToPreviousClient() throws InterruptedException{ + try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); + + assertDoesNotThrow(() -> tested.join(STREAMER)); + + tested.addChatMessageListener(chatMessageListener); + + assertThat(tested.getClientCount()).isEqualTo(1); + + verify(client).addWebSocketClosedListener(tested); + verify(client).connectBlocking(); + verify(client).join(STREAMER_LOWER); + verify(client).addChatMessageListener(chatMessageListener); + } + } + @Test void addExistingChannelToExistingClient() throws InterruptedException{ try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ - twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin)).thenReturn(client); + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); when(client.isChannelJoined(STREAMER_LOWER)).thenReturn(true); @@ -93,16 +148,53 @@ void addExistingChannelToExistingClient() throws InterruptedException{ assertThat(tested.getClientCount()).isEqualTo(1); - verify(client).addListener(tested); + verify(client).addWebSocketClosedListener(tested); + verify(client).connectBlocking(); + verify(client).join(STREAMER_LOWER); + } + } + + @Test + void addExistingListenerToNewClient() throws InterruptedException{ + try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); + + tested.addChatMessageListener(chatMessageListener); + + assertDoesNotThrow(() -> tested.join(STREAMER)); + + assertThat(tested.getClientCount()).isEqualTo(1); + + verify(client).addWebSocketClosedListener(tested); + verify(client).connectBlocking(); + verify(client).join(STREAMER_LOWER); + verify(client).addChatMessageListener(chatMessageListener); + } + } + + @Test + void addNewListenerToNewClient() throws InterruptedException{ + try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); + + assertDoesNotThrow(() -> tested.join(STREAMER)); + + assertThat(tested.getClientCount()).isEqualTo(1); + + verify(client).addWebSocketClosedListener(tested); verify(client).connectBlocking(); verify(client).join(STREAMER_LOWER); + verify(client, never()).addChatMessageListener(chatMessageListener); + + tested.addChatMessageListener(chatMessageListener); + verify(client).addChatMessageListener(chatMessageListener); } } @Test void clientError() throws InterruptedException{ try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ - twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin)).thenReturn(client); + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); doThrow(new RuntimeException("For tests")).when(client).connectBlocking(); @@ -115,7 +207,7 @@ void clientError() throws InterruptedException{ @Test void clientErrorJoinPending() throws InterruptedException{ try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ - twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin)).thenReturn(client); + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); doThrow(new RuntimeException("For tests")).when(client).connectBlocking(); @@ -128,7 +220,7 @@ void clientErrorJoinPending() throws InterruptedException{ assertThat(tested.getClientCount()).isEqualTo(1); - verify(client, times(2)).addListener(tested); + verify(client, times(2)).addWebSocketClosedListener(tested); verify(client, times(2)).connectBlocking(); verify(client).join(STREAMER_LOWER); } @@ -137,7 +229,7 @@ void clientErrorJoinPending() throws InterruptedException{ @Test void normalClientCloseRemovesClient(){ try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ - twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin)).thenReturn(client); + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); assertDoesNotThrow(() -> tested.join(STREAMER)); assertThat(tested.getClientCount()).isEqualTo(1); @@ -151,7 +243,7 @@ void normalClientCloseRemovesClient(){ void abnormalClientCloseRecreatesClient(){ try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ var client2 = mock(TwitchChatWebSocketClient.class); - twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin)).thenReturn(client).thenReturn(client2); + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client).thenReturn(client2); when(client.getChannels()).thenReturn(Set.of(STREAMER)); @@ -170,7 +262,7 @@ void abnormalClientCloseRecreatesClient(){ void pingSendsPing(){ try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class); var timeFactory = Mockito.mockStatic(TimeFactory.class)){ - twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin)).thenReturn(client); + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); timeFactory.when(TimeFactory::now).thenReturn(NOW); when(client.getLastHeartbeat()).thenReturn(NOW.minusSeconds(10)); @@ -190,7 +282,7 @@ void pingSendsPing(){ void closeTimedOut(){ try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class); var timeFactory = Mockito.mockStatic(TimeFactory.class)){ - twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin)).thenReturn(client); + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); timeFactory.when(TimeFactory::now).thenReturn(NOW); when(client.getLastHeartbeat()).thenReturn(NOW.minusSeconds(600)); @@ -208,7 +300,7 @@ void closeTimedOut(){ void keepNotTimedOut(){ try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class); var timeFactory = Mockito.mockStatic(TimeFactory.class)){ - twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin)).thenReturn(client); + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); timeFactory.when(TimeFactory::now).thenReturn(NOW); when(client.getLastHeartbeat()).thenReturn(NOW.minusSeconds(300)); @@ -225,7 +317,7 @@ void keepNotTimedOut(){ @Test void removeChannel(){ try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ - twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin)).thenReturn(client); + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); when(client.isChannelJoined(STREAMER_LOWER)).thenReturn(true); tested.join(STREAMER); @@ -238,7 +330,7 @@ void removeChannel(){ @Test void removeUnknownChannel(){ try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ - twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin)).thenReturn(client); + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); when(client.isChannelJoined(STREAMER_LOWER)).thenReturn(false); tested.join(STREAMER); @@ -251,7 +343,7 @@ void removeUnknownChannel(){ @Test void closeClosesClients(){ try(var twitchClientFactory = Mockito.mockStatic(TwitchWebSocketClientFactory.class)){ - twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin)).thenReturn(client); + twitchClientFactory.when(() -> TwitchWebSocketClientFactory.createChatClient(twitchLogin, false)).thenReturn(client); tested.join(STREAMER); assertDoesNotThrow(() -> tested.close()); diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/ws/TwitchPubSubWebSocketPoolTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/ws/TwitchPubSubWebSocketPoolTest.java index 41e1cf6e..9a296a8c 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/ws/TwitchPubSubWebSocketPoolTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/ws/TwitchPubSubWebSocketPoolTest.java @@ -1,6 +1,6 @@ package fr.raksrinana.channelpointsminer.miner.api.ws; -import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IMessage; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IPubSubMessage; import fr.raksrinana.channelpointsminer.miner.api.ws.data.request.topic.Topic; import fr.raksrinana.channelpointsminer.miner.api.ws.data.request.topic.Topics; import fr.raksrinana.channelpointsminer.miner.api.ws.data.response.ITwitchWebSocketResponse; @@ -218,7 +218,7 @@ void abnormalClientCloseRecreatesClient2(){ void messagesAreRedirected(){ var response = mock(MessageResponse.class); var data = mock(MessageData.class); - var message = mock(IMessage.class); + var message = mock(IPubSubMessage.class); when(response.getData()).thenReturn(data); when(data.getMessage()).thenReturn(message); diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/response/MessageDataTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/response/MessageDataTest.java index c05cc77a..0581e18e 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/response/MessageDataTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/ws/data/response/MessageDataTest.java @@ -1,6 +1,6 @@ package fr.raksrinana.channelpointsminer.miner.api.ws.data.response; -import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IMessage; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IPubSubMessage; import fr.raksrinana.channelpointsminer.miner.util.json.JacksonUtils; import org.mockito.Mock; import org.mockito.Mockito; @@ -17,7 +17,7 @@ class MessageDataTest{ private static final String JSON_CONTENT = "json-content"; @Mock - private IMessage message; + private IPubSubMessage message; @Test void setMessage(){ diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/database/DatabaseHandlerTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/database/DatabaseEventHandlerTest.java similarity index 65% rename from miner/src/test/java/fr/raksrinana/channelpointsminer/miner/database/DatabaseHandlerTest.java rename to miner/src/test/java/fr/raksrinana/channelpointsminer/miner/database/DatabaseEventHandlerTest.java index 365326e9..6a4e899b 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/database/DatabaseHandlerTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/database/DatabaseEventHandlerTest.java @@ -4,9 +4,16 @@ import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.pointsearned.PointsEarnedData; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.pointsspent.PointsSpentData; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.predictionresult.PredictionResultData; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Badge; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.EventStatus; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.PointGain; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.PointReasonCode; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Prediction; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Predictor; +import fr.raksrinana.channelpointsminer.miner.event.impl.ChatMessageEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.EventUpdatedEvent; import fr.raksrinana.channelpointsminer.miner.event.impl.PointsEarnedEvent; import fr.raksrinana.channelpointsminer.miner.event.impl.PointsSpentEvent; import fr.raksrinana.channelpointsminer.miner.event.impl.PredictionMadeEvent; @@ -19,16 +26,21 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.sql.SQLException; import java.time.Instant; +import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -36,18 +48,73 @@ @ParallelizableTest @ExtendWith(MockitoExtension.class) -class DatabaseHandlerTest{ +class DatabaseEventHandlerTest{ private static final String CHANNEL_ID = "channel-id"; private static final String CHANNEL_NAME = "channel-name"; private static final String EVENT_ID = "event-id"; private static final Instant NOW = Instant.parse("2020-02-18T12:47:52.000Z"); private static final int BALANCE = 12324; + private static final String BADGE_1 = "blue-1"; + private static final String BADGE_2 = "pink-2"; + private static final String USERNAME1 = "username1"; + private static final String USERNAME2 = "username2"; + private static final String USERNAME3 = "username3"; + private static final String USERNAME4 = "username4"; + private static final long BLUE_POINTS = 1L; + private static final long PINK_POINTS = 2L; + private static final String BLUE_TITLE = "blue"; + private static final String PINK_TITLE = "pink"; + private final static String ACTOR = "username"; + private final static String PREDICTION = "color"; + private final static String BADGE_PREDICTION_INFO = "badges=predictions/color,sub"; + private final static String BADGE_NO_PREDICTION_INFO = "badges=sub"; @InjectMocks - private DatabaseHandler tested; + private DatabaseEventHandler tested; @Mock private IDatabase database; + @Mock + private Event eventData; + @Mock + private Outcome blueOutcome; + @Mock + private Outcome pinkOutcome; + + @BeforeEach + void setUp(){ + lenient().when(eventData.getOutcomes()).thenReturn(List.of(blueOutcome, pinkOutcome)); + lenient().when(eventData.getChannelId()).thenReturn(CHANNEL_ID); + lenient().when(eventData.getId()).thenReturn(EVENT_ID); + lenient().when(eventData.getWinningOutcomeId()).thenReturn("blue-id"); + + lenient().when(blueOutcome.getTitle()).thenReturn(BLUE_TITLE); + lenient().when(blueOutcome.getId()).thenReturn("blue-id"); + lenient().when(blueOutcome.getTotalPoints()).thenReturn(BLUE_POINTS); + lenient().when(pinkOutcome.getTitle()).thenReturn(PINK_TITLE); + lenient().when(pinkOutcome.getId()).thenReturn("pink-id"); + lenient().when(pinkOutcome.getTotalPoints()).thenReturn(PINK_POINTS); + + var blueBadge = mock(Badge.class); + var pinkBadge = mock(Badge.class); + + lenient().when(blueOutcome.getBadge()).thenReturn(blueBadge); + lenient().when(pinkOutcome.getBadge()).thenReturn(pinkBadge); + lenient().when(blueBadge.getVersion()).thenReturn(BADGE_1); + lenient().when(pinkBadge.getVersion()).thenReturn(BADGE_2); + + var predictor1 = mock(Predictor.class); + var predictor2 = mock(Predictor.class); + var predictor3 = mock(Predictor.class); + var predictor4 = mock(Predictor.class); + + lenient().when(predictor1.getUserDisplayName()).thenReturn(USERNAME1); + lenient().when(predictor2.getUserDisplayName()).thenReturn(USERNAME2); + lenient().when(predictor3.getUserDisplayName()).thenReturn(USERNAME3); + lenient().when(predictor4.getUserDisplayName()).thenReturn(USERNAME4); + lenient().when(blueOutcome.getTopPredictors()).thenReturn(List.of(predictor1, predictor2)); + lenient().when(pinkOutcome.getTopPredictors()).thenReturn(List.of(predictor3, predictor4)); + } @Test void onStreamerAdded() throws SQLException{ @@ -300,4 +367,79 @@ void closeClosesDatabase(){ verify(database).close(); } + + @Test + void onActivePredictionUpdate() throws SQLException{ + var event = mock(EventUpdatedEvent.class); + + when(event.getEvent()).thenReturn(eventData); + when(event.getStreamerUsername()).thenReturn(CHANNEL_NAME); + + when(eventData.getStatus()).thenReturn(EventStatus.ACTIVE); + + assertDoesNotThrow(() -> tested.onEvent(event)); + + verify(database).addUserPrediction(USERNAME1, CHANNEL_NAME, BADGE_1); + verify(database).addUserPrediction(USERNAME2, CHANNEL_NAME, BADGE_1); + verify(database).addUserPrediction(USERNAME3, CHANNEL_NAME, BADGE_2); + verify(database).addUserPrediction(USERNAME4, CHANNEL_NAME, BADGE_2); + } + + @Test + void onCancelledPredictionUpdate() throws SQLException{ + var event = mock(EventUpdatedEvent.class); + + when(event.getEvent()).thenReturn(eventData); + when(event.getStreamerUsername()).thenReturn(CHANNEL_NAME); + + when(eventData.getStatus()).thenReturn(EventStatus.CANCELED); + + assertDoesNotThrow(() -> tested.onEvent(event)); + + verify(database).cancelPrediction(eventData); + } + + @Test + void onResolvedPredictionUpdate() throws SQLException{ + var event = mock(EventUpdatedEvent.class); + + when(event.getEvent()).thenReturn(eventData); + when(event.getStreamerUsername()).thenReturn(CHANNEL_NAME); + + when(eventData.getStatus()).thenReturn(EventStatus.RESOLVED); + + assertDoesNotThrow(() -> tested.onEvent(event)); + + var returnRatio = (double) (BLUE_POINTS + PINK_POINTS) / BLUE_POINTS; + + verify(database).resolvePrediction(eventData, BLUE_TITLE, BADGE_1, returnRatio); + } + + @Test + void onChatMessagePredictionRecorded() throws SQLException{ + var event = mock(ChatMessageEvent.class); + + when(event.getStreamer()).thenReturn(CHANNEL_NAME); + when(event.getActor()).thenReturn(ACTOR); + when(event.getBadges()).thenReturn(BADGE_PREDICTION_INFO); + + assertDoesNotThrow(() -> tested.onEvent(event)); + + verify(database).addUserPrediction(ACTOR, CHANNEL_NAME, PREDICTION); + } + + @ParameterizedTest + @ValueSource(strings = { + "", + BADGE_NO_PREDICTION_INFO + }) + void onChatMessageNoPredictionRecorded(String badges) throws SQLException{ + var event = mock(ChatMessageEvent.class); + + when(event.getBadges()).thenReturn(badges); + + assertDoesNotThrow(() -> tested.onEvent(event)); + + verify(database, never()).addUserPrediction(any(), any(), any()); + } } \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/event/AbstractLogEventTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/event/AbstractLogEventTest.java index 9ea48b97..e59f75e7 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/event/AbstractLogEventTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/event/AbstractLogEventTest.java @@ -20,7 +20,7 @@ @ParallelizableTest @ExtendWith(MockitoExtension.class) class AbstractLogEventTest{ - private AbstractEvent tested; + private AbstractLoggableEvent tested; @Mock private IMiner miner; diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/ConfigurationFactoryTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/ConfigurationFactoryTest.java index a57be06c..27a532bf 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/ConfigurationFactoryTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/ConfigurationFactoryTest.java @@ -105,10 +105,12 @@ void getInstanceOverridden() throws MalformedURLException{ .reloadEvery(15) .analytics(AnalyticsConfiguration.builder() .enabled(true) + .recordChatsPredictions(true) .database(DatabaseConfiguration.builder() .jdbcUrl("jdbcUrl") .username("user") .password("pass") + .maxPoolSize(15) .build()) .build()) .build())) diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/DatabaseFactoryTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/DatabaseFactoryTest.java index 86e4d0a1..7a1398a0 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/DatabaseFactoryTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/DatabaseFactoryTest.java @@ -2,13 +2,15 @@ import fr.raksrinana.channelpointsminer.miner.config.DatabaseConfiguration; import fr.raksrinana.channelpointsminer.miner.database.IDatabase; +import fr.raksrinana.channelpointsminer.miner.database.NoOpDatabase; import fr.raksrinana.channelpointsminer.miner.tests.ParallelizableTest; -import org.assertj.core.api.Assertions; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import java.sql.SQLException; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -33,6 +35,11 @@ void setUp(){ lenient().when(databaseConfiguration.getPassword()).thenReturn(PASSWORD); } + @Test + void createNoOpDatabase() throws SQLException{ + assertThat(DatabaseFactory.createDatabase(null)).isInstanceOf(NoOpDatabase.class); + } + @Test void createDatabaseException(){ assertThrows(Exception.class, () -> DatabaseFactory.createDatabase(databaseConfiguration)); @@ -57,6 +64,6 @@ void createDatabaseHandler(){ var database = mock(IDatabase.class); var handler = DatabaseFactory.createDatabaseHandler(database); - Assertions.assertThat(handler).isNotNull(); + assertThat(handler).isNotNull(); } } \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/MinerFactoryTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/MinerFactoryTest.java index 9fc8946f..4e92c9fe 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/MinerFactoryTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/MinerFactoryTest.java @@ -6,7 +6,7 @@ import fr.raksrinana.channelpointsminer.miner.config.AnalyticsConfiguration; import fr.raksrinana.channelpointsminer.miner.config.DatabaseConfiguration; import fr.raksrinana.channelpointsminer.miner.config.DiscordConfiguration; -import fr.raksrinana.channelpointsminer.miner.database.DatabaseHandler; +import fr.raksrinana.channelpointsminer.miner.database.DatabaseEventHandler; import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.handler.ClaimAvailableHandler; import fr.raksrinana.channelpointsminer.miner.handler.FollowRaidHandler; @@ -30,6 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ParallelizableTest @@ -55,7 +56,7 @@ class MinerFactoryTest{ @Mock private IDatabase database; @Mock - private DatabaseHandler databaseHandler; + private DatabaseEventHandler databaseEventHandler; @BeforeEach void setUp(){ @@ -65,7 +66,6 @@ void setUp(){ lenient().when(accountConfiguration.isUse2Fa()).thenReturn(USE_2FA); lenient().when(accountConfiguration.getDiscord()).thenReturn(discordConfiguration); lenient().when(accountConfiguration.getAnalytics()).thenReturn(analyticsConfiguration); - lenient().when(analyticsConfiguration.getDatabase()).thenReturn(databaseConfiguration); } @Test @@ -75,7 +75,7 @@ void nominal(){ var miner = MinerFactory.create(accountConfiguration); - Assertions.assertThat(miner.getMessageHandlers()) + Assertions.assertThat(miner.getPubSubMessageHandlers()) .hasSize(5) .hasAtLeastOneElementOfType(ClaimAvailableHandler.class) .hasAtLeastOneElementOfType(StreamStartEndHandler.class) @@ -83,7 +83,7 @@ void nominal(){ .hasAtLeastOneElementOfType(PredictionsHandler.class) .hasAtLeastOneElementOfType(PointsHandler.class); - Assertions.assertThat(miner.getEventListeners()) + Assertions.assertThat(miner.getEventHandlers()) .hasSize(1) .hasAtLeastOneElementOfType(LoggerEventListener.class); @@ -103,7 +103,7 @@ void nominalWithDiscord() throws MalformedURLException{ var miner = MinerFactory.create(accountConfiguration); - Assertions.assertThat(miner.getMessageHandlers()) + Assertions.assertThat(miner.getPubSubMessageHandlers()) .hasSize(5) .hasAtLeastOneElementOfType(ClaimAvailableHandler.class) .hasAtLeastOneElementOfType(StreamStartEndHandler.class) @@ -111,7 +111,7 @@ void nominalWithDiscord() throws MalformedURLException{ .hasAtLeastOneElementOfType(PredictionsHandler.class) .hasAtLeastOneElementOfType(PointsHandler.class); - Assertions.assertThat(miner.getEventListeners()) + Assertions.assertThat(miner.getEventHandlers()) .hasSize(2) .hasAtLeastOneElementOfType(LoggerEventListener.class) .hasAtLeastOneElementOfType(DiscordEventListener.class); @@ -121,18 +121,19 @@ void nominalWithDiscord() throws MalformedURLException{ } @Test - void nominalWithAnalytics(){ + void nominalWithAnalytics() throws SQLException{ try(var apiFactory = mockStatic(ApiFactory.class); var databaseFactory = mockStatic(DatabaseFactory.class)){ apiFactory.when(() -> ApiFactory.createPassportApi(USERNAME, PASSWORD, AUTH_FOLDER, USE_2FA)).thenReturn(passportApi); databaseFactory.when(() -> DatabaseFactory.createDatabase(databaseConfiguration)).thenReturn(database); - databaseFactory.when(() -> DatabaseFactory.createDatabaseHandler(database)).thenReturn(databaseHandler); + databaseFactory.when(() -> DatabaseFactory.createDatabaseHandler(database)).thenReturn(databaseEventHandler); when(analyticsConfiguration.isEnabled()).thenReturn(true); + when(analyticsConfiguration.getDatabase()).thenReturn(databaseConfiguration); var miner = MinerFactory.create(accountConfiguration); - Assertions.assertThat(miner.getMessageHandlers()) + Assertions.assertThat(miner.getPubSubMessageHandlers()) .hasSize(5) .hasAtLeastOneElementOfType(ClaimAvailableHandler.class) .hasAtLeastOneElementOfType(StreamStartEndHandler.class) @@ -140,10 +141,12 @@ void nominalWithAnalytics(){ .hasAtLeastOneElementOfType(PredictionsHandler.class) .hasAtLeastOneElementOfType(PointsHandler.class); - Assertions.assertThat(miner.getEventListeners()) + Assertions.assertThat(miner.getEventHandlers()) .hasSize(2) .hasAtLeastOneElementOfType(LoggerEventListener.class) - .hasAtLeastOneElementOfType(DatabaseHandler.class); + .hasAtLeastOneElementOfType(DatabaseEventHandler.class); + + verify(database).deleteUserPredictions(); miner.close(); } @@ -154,9 +157,7 @@ void nominalWithAnalyticsException(){ try(var apiFactory = mockStatic(ApiFactory.class); var databaseFactory = mockStatic(DatabaseFactory.class)){ apiFactory.when(() -> ApiFactory.createPassportApi(USERNAME, PASSWORD, AUTH_FOLDER, USE_2FA)).thenReturn(passportApi); - databaseFactory.when(() -> DatabaseFactory.createDatabase(databaseConfiguration)).thenThrow(new SQLException("For tests")); - - when(analyticsConfiguration.isEnabled()).thenReturn(true); + databaseFactory.when(() -> DatabaseFactory.createDatabase(null)).thenThrow(new SQLException("For tests")); assertThrows(IllegalStateException.class, () -> MinerFactory.create(accountConfiguration)); } diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/MessageHandlerFactoryTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/PubSubMessageHandlerFactoryTest.java similarity index 63% rename from miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/MessageHandlerFactoryTest.java rename to miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/PubSubMessageHandlerFactoryTest.java index 17ebe688..74099a10 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/MessageHandlerFactoryTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/PubSubMessageHandlerFactoryTest.java @@ -16,7 +16,7 @@ @ParallelizableTest @ExtendWith(MockitoExtension.class) -class MessageHandlerFactoryTest{ +class PubSubMessageHandlerFactoryTest{ @Mock private IMiner miner; @Mock @@ -24,26 +24,26 @@ class MessageHandlerFactoryTest{ @Test void createClaimAvailable(){ - assertThat(MessageHandlerFactory.createClaimAvailableHandler(miner)).isNotNull().isInstanceOf(ClaimAvailableHandler.class); + assertThat(PubSubMessageHandlerFactory.createClaimAvailableHandler(miner)).isNotNull().isInstanceOf(ClaimAvailableHandler.class); } @Test void createStreamStartEndHandler(){ - assertThat(MessageHandlerFactory.createStreamStartEndHandler(miner)).isNotNull().isInstanceOf(StreamStartEndHandler.class); + assertThat(PubSubMessageHandlerFactory.createStreamStartEndHandler(miner)).isNotNull().isInstanceOf(StreamStartEndHandler.class); } @Test void createFollowRaidHandler(){ - assertThat(MessageHandlerFactory.createFollowRaidHandler(miner)).isNotNull().isInstanceOf(FollowRaidHandler.class); + assertThat(PubSubMessageHandlerFactory.createFollowRaidHandler(miner)).isNotNull().isInstanceOf(FollowRaidHandler.class); } @Test void createPredictionsHandler(){ - assertThat(MessageHandlerFactory.createPredictionsHandler(miner, betPlacer)).isNotNull().isInstanceOf(PredictionsHandler.class); + assertThat(PubSubMessageHandlerFactory.createPredictionsHandler(miner, betPlacer)).isNotNull().isInstanceOf(PredictionsHandler.class); } @Test void createPointsHandler(){ - assertThat(MessageHandlerFactory.createPointsHandler(miner)).isNotNull().isInstanceOf(PointsHandler.class); + assertThat(PubSubMessageHandlerFactory.createPointsHandler(miner)).isNotNull().isInstanceOf(PointsHandler.class); } } \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/TwitchChatFactoryTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/TwitchChatFactoryTest.java similarity index 57% rename from miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/TwitchChatFactoryTest.java rename to miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/TwitchChatFactoryTest.java index adbc04bd..f691ddb0 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/api/chat/TwitchChatFactoryTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/TwitchChatFactoryTest.java @@ -1,29 +1,41 @@ -package fr.raksrinana.channelpointsminer.miner.api.chat; +package fr.raksrinana.channelpointsminer.miner.factory; import fr.raksrinana.channelpointsminer.miner.api.chat.irc.TwitchIrcChatClient; import fr.raksrinana.channelpointsminer.miner.api.chat.ws.TwitchChatWebSocketPool; import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin; import fr.raksrinana.channelpointsminer.miner.config.ChatMode; +import fr.raksrinana.channelpointsminer.miner.miner.IMiner; import fr.raksrinana.channelpointsminer.miner.tests.ParallelizableTest; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; @ParallelizableTest @ExtendWith(MockitoExtension.class) class TwitchChatFactoryTest{ + + @Mock + private IMiner miner; @Mock private TwitchLogin twitchLogin; + @BeforeEach + void setUp(){ + when(miner.getTwitchLogin()).thenReturn(twitchLogin); + } + @Test void createIrcChat(){ - assertThat(TwitchChatFactory.createChat(ChatMode.IRC, twitchLogin)).isNotNull().isInstanceOf(TwitchIrcChatClient.class); + assertThat(TwitchChatFactory.createChat(miner, ChatMode.IRC, false)).isNotNull() + .isInstanceOf(TwitchIrcChatClient.class); } @Test void createWsChat(){ - assertThat(TwitchChatFactory.createChat(ChatMode.WS, twitchLogin)).isNotNull().isInstanceOf(TwitchChatWebSocketPool.class); + assertThat(TwitchChatFactory.createChat(miner, ChatMode.WS, false)).isNotNull().isInstanceOf(TwitchChatWebSocketPool.class); } } \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/TwitchPubSubWebSocketClientFactoryTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/TwitchPubSubWebSocketClientFactoryTest.java index 6a7bc619..5cd56bfd 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/TwitchPubSubWebSocketClientFactoryTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/factory/TwitchPubSubWebSocketClientFactoryTest.java @@ -20,6 +20,7 @@ void createPubSub(){ @Test void createChat(){ - Assertions.assertThat(TwitchWebSocketClientFactory.createChatClient(twitchLogin)).isNotNull().isInstanceOf(TwitchChatWebSocketClient.class); + Assertions.assertThat(TwitchWebSocketClientFactory.createChatClient(twitchLogin, true)).isNotNull() + .isInstanceOf(TwitchChatWebSocketClient.class); } } \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/handler/PredictionsHandlerEventCreatedTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/handler/PredictionsHandlerEventCreatedTest.java index 1f2cf452..8dfe6914 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/handler/PredictionsHandlerEventCreatedTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/handler/PredictionsHandlerEventCreatedTest.java @@ -45,6 +45,7 @@ @ExtendWith(MockitoExtension.class) class PredictionsHandlerEventCreatedTest{ private static final String STREAMER_ID = "streamer-id"; + private static final String NON_EXISTENT_STREAMER_ID = "streamer-id2"; private static final String EVENT_ID = "event-id"; private static final int MINIMUM_REQUIRED = 50; private static final int WINDOW_SECONDS = 300; @@ -78,12 +79,13 @@ class PredictionsHandlerEventCreatedTest{ @BeforeEach void setUp(){ + lenient().when(topic.getTarget()).thenReturn(STREAMER_ID); lenient().when(miner.getStreamerById(STREAMER_ID)).thenReturn(Optional.of(streamer)); lenient().when(eventCreated.getData()).thenReturn(eventCreatedData); lenient().when(eventCreatedData.getEvent()).thenReturn(event); - + lenient().when(event.getId()).thenReturn(EVENT_ID); lenient().when(event.getStatus()).thenReturn(ACTIVE); lenient().when(event.getCreatedAt()).thenReturn(EVENT_DATE); diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/handler/PredictionsHandlerEventUpdatedTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/handler/PredictionsHandlerEventUpdatedTest.java index bda1efc4..ee8e6d5f 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/handler/PredictionsHandlerEventUpdatedTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/handler/PredictionsHandlerEventUpdatedTest.java @@ -6,6 +6,7 @@ import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.EventStatus; import fr.raksrinana.channelpointsminer.miner.api.ws.data.request.topic.Topic; import fr.raksrinana.channelpointsminer.miner.event.impl.EventCreatedEvent; +import fr.raksrinana.channelpointsminer.miner.event.impl.EventUpdatedEvent; import fr.raksrinana.channelpointsminer.miner.factory.TimeFactory; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.handler.data.PredictionState; @@ -22,6 +23,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import java.time.Instant; import java.time.ZonedDateTime; import java.util.Map; import java.util.Optional; @@ -36,6 +38,7 @@ import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -43,13 +46,15 @@ @ExtendWith(MockitoExtension.class) class PredictionsHandlerEventUpdatedTest{ private static final String STREAMER_ID = "streamer-id"; + private static final String STREAMER_USERNAME = "streamer-username"; private static final String EVENT_ID = "event-id"; private static final int MINIMUM_REQUIRED = 50; private static final int WINDOW_SECONDS = 300; private static final ZonedDateTime EVENT_DATE = ZonedDateTime.of(2021, 10, 10, 11, 59, 0, 0, UTC); private static final ZonedDateTime EVENT_UPDATE_DATE = ZonedDateTime.of(2021, 10, 10, 11, 59, 30, 0, UTC); private static final ZonedDateTime SCHEDULE_DATE = ZonedDateTime.of(2021, 10, 10, 12, 1, 0, 0, UTC); - private static final ZonedDateTime NOW = ZonedDateTime.of(2021, 10, 10, 12, 0, 0, 0, UTC); + private static final Instant NOW = Instant.parse("2021-10-10T12:00:00.000Z"); + private static final ZonedDateTime NOW_ZONED = ZonedDateTime.ofInstant(NOW, UTC); @InjectMocks private PredictionsHandler tested; @@ -65,8 +70,6 @@ class PredictionsHandlerEventUpdatedTest{ @Mock private Event event; @Mock - private Event event2; - @Mock private Topic topic; @Mock private Streamer streamer; @@ -84,16 +87,16 @@ void setUp(){ lenient().when(miner.getStreamerById(STREAMER_ID)).thenReturn(Optional.of(streamer)); lenient().when(eventUpdated.getData()).thenReturn(eventUpdatedData); - lenient().when(eventUpdatedData.getEvent()).thenReturn(event2); + lenient().when(eventUpdatedData.getEvent()).thenReturn(event); lenient().when(eventUpdatedData.getTimestamp()).thenReturn(EVENT_UPDATE_DATE); lenient().when(event.getId()).thenReturn(EVENT_ID); - lenient().when(event2.getId()).thenReturn(EVENT_ID); - lenient().when(event2.getStatus()).thenReturn(EventStatus.ACTIVE); - lenient().when(event2.getCreatedAt()).thenReturn(EVENT_DATE); - lenient().when(event2.getPredictionWindowSeconds()).thenReturn(WINDOW_SECONDS); + lenient().when(event.getStatus()).thenReturn(EventStatus.ACTIVE); + lenient().when(event.getCreatedAt()).thenReturn(EVENT_DATE); + lenient().when(event.getPredictionWindowSeconds()).thenReturn(WINDOW_SECONDS); lenient().when(streamer.getId()).thenReturn(STREAMER_ID); + lenient().when(streamer.getUsername()).thenReturn(STREAMER_USERNAME); lenient().when(streamer.getSettings()).thenReturn(streamerSettings); lenient().when(streamer.getChannelPoints()).thenReturn(Optional.of(MINIMUM_REQUIRED + 1)); @@ -101,7 +104,7 @@ void setUp(){ lenient().when(predictionSettings.getMinimumPointsRequired()).thenReturn(MINIMUM_REQUIRED); lenient().when(predictionSettings.getDelayCalculator()).thenReturn(delayCalculator); - lenient().when(delayCalculator.calculate(event2)).thenReturn(SCHEDULE_DATE); + lenient().when(delayCalculator.calculate(event)).thenReturn(SCHEDULE_DATE); lenient().when(miner.schedule(any(Runnable.class), anyLong(), any())).thenAnswer(invocation -> { var runnable = invocation.getArgument(0, Runnable.class); @@ -113,10 +116,11 @@ void setUp(){ @Test void unknownEvent(){ try(var timeFactory = mockStatic(TimeFactory.class)){ - timeFactory.when(TimeFactory::nowZoned).thenReturn(NOW); - + timeFactory.when(TimeFactory::nowZoned).thenReturn(NOW_ZONED); + timeFactory.when(TimeFactory::now).thenReturn(NOW); + var expectedPrediction = BettingPrediction.builder() - .event(event2) + .event(event) .streamer(streamer) .state(PredictionState.SCHEDULED) .lastUpdate(EVENT_DATE) @@ -126,7 +130,8 @@ void unknownEvent(){ assertThat(tested.getPredictions()).containsOnly(Map.entry(EVENT_ID, expectedPrediction)); verify(miner).schedule(any(), eq(60L), eq(TimeUnit.SECONDS)); - verify(miner).onEvent(new EventCreatedEvent(miner, streamer, event2)); + verify(miner).onEvent(new EventCreatedEvent(miner, streamer, event)); + verify(miner).onEvent(new EventUpdatedEvent(miner, NOW, STREAMER_USERNAME, event)); verify(betPlacer).placeBet(expectedPrediction); } } @@ -145,7 +150,7 @@ void updatePrediction(){ assertDoesNotThrow(() -> tested.handle(topic, eventUpdated)); assertThat(tested.getPredictions()).containsOnly(Map.entry(EVENT_ID, BettingPrediction.builder() - .event(event2) + .event(event) .streamer(streamer) .state(PredictionState.SCHEDULED) .lastUpdate(EVENT_UPDATE_DATE) @@ -174,4 +179,16 @@ void eventIsNotTheLatest(){ assertDoesNotThrow(() -> tested.handle(topic, eventUpdated)); assertThat(tested.getPredictions()).containsOnly(Map.entry(EVENT_ID, createDefaultPrediction())); } + + @Test + void eventUpdatedEvent(){ + try(var timeFactory = mockStatic(TimeFactory.class)){ + timeFactory.when(TimeFactory::now).thenReturn(NOW); + + createEvent(); + + assertDoesNotThrow(() -> tested.handle(topic, eventUpdated)); + verify(miner).onEvent(new EventUpdatedEvent(miner, NOW, STREAMER_USERNAME, event)); + } + } } \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/miner/MinerTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/miner/MinerTest.java index 6a8b8b6c..12b243a7 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/miner/MinerTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/miner/MinerTest.java @@ -1,7 +1,6 @@ package fr.raksrinana.channelpointsminer.miner.miner; import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatClient; -import fr.raksrinana.channelpointsminer.miner.api.chat.TwitchChatFactory; import fr.raksrinana.channelpointsminer.miner.api.gql.GQLApi; import fr.raksrinana.channelpointsminer.miner.api.passport.PassportApi; import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin; @@ -9,20 +8,23 @@ import fr.raksrinana.channelpointsminer.miner.api.passport.exceptions.LoginException; import fr.raksrinana.channelpointsminer.miner.api.twitch.TwitchApi; import fr.raksrinana.channelpointsminer.miner.api.ws.TwitchPubSubWebSocketPool; -import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IMessage; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.IPubSubMessage; import fr.raksrinana.channelpointsminer.miner.api.ws.data.request.topic.Topic; import fr.raksrinana.channelpointsminer.miner.api.ws.data.request.topic.Topics; import fr.raksrinana.channelpointsminer.miner.config.AccountConfiguration; +import fr.raksrinana.channelpointsminer.miner.config.AnalyticsConfiguration; import fr.raksrinana.channelpointsminer.miner.config.ChatMode; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.event.IEvent; -import fr.raksrinana.channelpointsminer.miner.event.IEventListener; +import fr.raksrinana.channelpointsminer.miner.event.IEventHandler; import fr.raksrinana.channelpointsminer.miner.event.impl.StreamerAddedEvent; import fr.raksrinana.channelpointsminer.miner.event.impl.StreamerRemovedEvent; import fr.raksrinana.channelpointsminer.miner.factory.ApiFactory; import fr.raksrinana.channelpointsminer.miner.factory.MinerRunnableFactory; import fr.raksrinana.channelpointsminer.miner.factory.StreamerSettingsFactory; import fr.raksrinana.channelpointsminer.miner.factory.TimeFactory; -import fr.raksrinana.channelpointsminer.miner.handler.IMessageHandler; +import fr.raksrinana.channelpointsminer.miner.factory.TwitchChatFactory; +import fr.raksrinana.channelpointsminer.miner.handler.IPubSubMessageHandler; import fr.raksrinana.channelpointsminer.miner.runnable.StreamerConfigurationReload; import fr.raksrinana.channelpointsminer.miner.runnable.SyncInventory; import fr.raksrinana.channelpointsminer.miner.runnable.UpdateStreamInfo; @@ -79,6 +81,8 @@ class MinerTest{ @Mock private AccountConfiguration accountConfiguration; @Mock + private AnalyticsConfiguration analyticsConfiguration; + @Mock private PassportApi passportApi; @Mock private TwitchPubSubWebSocketPool webSocketPool; @@ -88,6 +92,8 @@ class MinerTest{ private ScheduledExecutorService scheduledExecutorService; @Mock private ExecutorService executorService; + @Mock + private IDatabase database; @Mock private TwitchLogin twitchLogin; @@ -108,15 +114,18 @@ class MinerTest{ @Mock private StreamerConfigurationReload streamerConfigurationReload; @Mock - private IEventListener eventListener; + private IEventHandler eventHandler; @BeforeEach void setUp() throws LoginException, IOException{ - tested = new Miner(accountConfiguration, passportApi, streamerSettingsFactory, webSocketPool, scheduledExecutorService, executorService); + tested = new Miner(accountConfiguration, passportApi, streamerSettingsFactory, webSocketPool, scheduledExecutorService, executorService, database); lenient().when(accountConfiguration.getReloadEvery()).thenReturn(0); lenient().when(accountConfiguration.isLoadFollows()).thenReturn(false); lenient().when(accountConfiguration.getChatMode()).thenReturn(CHAT_MODE); + lenient().when(accountConfiguration.getAnalytics()).thenReturn(analyticsConfiguration); + lenient().when(analyticsConfiguration.isEnabled()).thenReturn(false); + lenient().when(analyticsConfiguration.isRecordChatsPredictions()).thenReturn(false); lenient().when(passportApi.login()).thenReturn(twitchLogin); lenient().when(streamerSettings.isFollowRaid()).thenReturn(false); @@ -139,7 +148,7 @@ void setupIsDoneWithNoConfigReload() throws LoginException, IOException{ var ircFactory = mockStatic(TwitchChatFactory.class)){ apiFactory.when(ApiFactory::createTwitchApi).thenReturn(twitchApi); apiFactory.when(() -> ApiFactory.createGqlApi(twitchLogin)).thenReturn(gqlApi); - ircFactory.when(() -> TwitchChatFactory.createChat(CHAT_MODE, twitchLogin)).thenReturn(twitchChatClient); + ircFactory.when(() -> TwitchChatFactory.createChat(tested, CHAT_MODE, false)).thenReturn(twitchChatClient); runnableFactory.when(() -> MinerRunnableFactory.createUpdateStreamInfo(tested)).thenReturn(updateStreamInfo); runnableFactory.when(() -> MinerRunnableFactory.createStreamerConfigurationReload(tested, streamerSettingsFactory, false)).thenReturn(streamerConfigurationReload); @@ -155,6 +164,7 @@ void setupIsDoneWithNoConfigReload() throws LoginException, IOException{ verify(webSocketPool).listenTopic(Topics.buildFromName(COMMUNITY_POINTS_USER_V1, USER_ID, ACCESS_TOKEN)); verify(twitchChatClient, never()).join(any()); verify(scheduledExecutorService).schedule(eq(streamerConfigurationReload), anyLong(), any()); + verify(twitchChatClient).addChatMessageListener(any()); } } @@ -207,6 +217,65 @@ void setupIsDoneWithConfigReloadAndFollows() throws LoginException, IOException{ } } + @Test + void setupIsDoneWithAnalytics() throws LoginException, IOException{ + try(var apiFactory = mockStatic(ApiFactory.class); + var runnableFactory = mockStatic(MinerRunnableFactory.class); + var ircFactory = mockStatic(TwitchChatFactory.class)){ + apiFactory.when(ApiFactory::createTwitchApi).thenReturn(twitchApi); + apiFactory.when(() -> ApiFactory.createGqlApi(twitchLogin)).thenReturn(gqlApi); + ircFactory.when(() -> TwitchChatFactory.createChat(tested, CHAT_MODE, false)).thenReturn(twitchChatClient); + + runnableFactory.when(() -> MinerRunnableFactory.createUpdateStreamInfo(tested)).thenReturn(updateStreamInfo); + runnableFactory.when(() -> MinerRunnableFactory.createStreamerConfigurationReload(tested, streamerSettingsFactory, false)).thenReturn(streamerConfigurationReload); + + when(analyticsConfiguration.isEnabled()).thenReturn(true); + + assertDoesNotThrow(() -> tested.start()); + + Assertions.assertThat(tested.getTwitchApi()).isEqualTo(twitchApi); + Assertions.assertThat(tested.getGqlApi()).isEqualTo(gqlApi); + Assertions.assertThat(tested.getChatClient()).isEqualTo(twitchChatClient); + Assertions.assertThat(tested.getStreamers()).isEmpty(); + + verify(passportApi).login(); + verify(webSocketPool).listenTopic(Topics.buildFromName(COMMUNITY_POINTS_USER_V1, USER_ID, ACCESS_TOKEN)); + verify(twitchChatClient, never()).join(any()); + verify(scheduledExecutorService).schedule(eq(streamerConfigurationReload), anyLong(), any()); + verify(twitchChatClient).addChatMessageListener(any()); + } + } + + @Test + void setupIsDoneWithAnalyticsAndPredictionRecording() throws LoginException, IOException{ + try(var apiFactory = mockStatic(ApiFactory.class); + var runnableFactory = mockStatic(MinerRunnableFactory.class); + var ircFactory = mockStatic(TwitchChatFactory.class)){ + apiFactory.when(ApiFactory::createTwitchApi).thenReturn(twitchApi); + apiFactory.when(() -> ApiFactory.createGqlApi(twitchLogin)).thenReturn(gqlApi); + ircFactory.when(() -> TwitchChatFactory.createChat(tested, CHAT_MODE, true)).thenReturn(twitchChatClient); + + runnableFactory.when(() -> MinerRunnableFactory.createUpdateStreamInfo(tested)).thenReturn(updateStreamInfo); + runnableFactory.when(() -> MinerRunnableFactory.createStreamerConfigurationReload(tested, streamerSettingsFactory, false)).thenReturn(streamerConfigurationReload); + + when(analyticsConfiguration.isEnabled()).thenReturn(true); + when(analyticsConfiguration.isRecordChatsPredictions()).thenReturn(true); + + assertDoesNotThrow(() -> tested.start()); + + Assertions.assertThat(tested.getTwitchApi()).isEqualTo(twitchApi); + Assertions.assertThat(tested.getGqlApi()).isEqualTo(gqlApi); + Assertions.assertThat(tested.getChatClient()).isEqualTo(twitchChatClient); + Assertions.assertThat(tested.getStreamers()).isEmpty(); + + verify(passportApi).login(); + verify(webSocketPool).listenTopic(Topics.buildFromName(COMMUNITY_POINTS_USER_V1, USER_ID, ACCESS_TOKEN)); + verify(twitchChatClient, never()).join(any()); + verify(scheduledExecutorService).schedule(eq(streamerConfigurationReload), anyLong(), any()); + verify(twitchChatClient).addChatMessageListener(any()); + } + } + @Test void captchaLogin() throws LoginException, IOException{ when(passportApi.login()).thenThrow(new CaptchaSolveRequired(400, -1, "For tests")); @@ -227,9 +296,9 @@ void close() throws Exception{ var ircFactory = mockStatic(TwitchChatFactory.class)){ apiFactory.when(ApiFactory::createTwitchApi).thenReturn(twitchApi); apiFactory.when(() -> ApiFactory.createGqlApi(twitchLogin)).thenReturn(gqlApi); - ircFactory.when(() -> TwitchChatFactory.createChat(CHAT_MODE, twitchLogin)).thenReturn(twitchChatClient); + ircFactory.when(() -> TwitchChatFactory.createChat(tested, CHAT_MODE, false)).thenReturn(twitchChatClient); - tested.addEventListener(eventListener); + tested.addEventHandler(eventHandler); tested.start(); assertDoesNotThrow(() -> tested.close()); @@ -238,13 +307,13 @@ void close() throws Exception{ verify(executorService).shutdown(); verify(webSocketPool).close(); verify(twitchChatClient).close(); - verify(eventListener).close(); + verify(eventHandler).close(); } } @Test void unknownMessageIsNotForwarded(){ - var message = mock(IMessage.class); + var message = mock(IPubSubMessage.class); assertDoesNotThrow(() -> tested.onTwitchMessage(topic, message)); verify(executorService, never()).submit(any(Runnable.class)); @@ -252,13 +321,13 @@ void unknownMessageIsNotForwarded(){ @Test void messageHandlersAreCalled(){ - var handler1 = mock(IMessageHandler.class); - var handler2 = mock(IMessageHandler.class); + var handler1 = mock(IPubSubMessageHandler.class); + var handler2 = mock(IPubSubMessageHandler.class); - tested.addHandler(handler1); - tested.addHandler(handler2); + tested.addPubSubHandler(handler1); + tested.addPubSubHandler(handler2); - var message = mock(IMessage.class); + var message = mock(IPubSubMessage.class); assertDoesNotThrow(() -> tested.onTwitchMessage(topic, message)); verify(executorService, times(2)).submit(any(Runnable.class)); @@ -280,7 +349,7 @@ void addStreamerWithPredictions(){ when(streamerSettings.isMakePredictions()).thenReturn(true); - tested.addEventListener(eventListener); + tested.addEventHandler(eventHandler); tested.start(); var streamer = mock(Streamer.class); @@ -298,7 +367,7 @@ void addStreamerWithPredictions(){ verify(webSocketPool).listenTopic(Topics.buildFromName(PREDICTIONS_USER_V1, USER_ID, ACCESS_TOKEN)); verify(webSocketPool).listenTopic(Topics.buildFromName(VIDEO_PLAYBACK_BY_ID, STREAMER_ID, ACCESS_TOKEN)); verify(webSocketPool).listenTopic(Topics.buildFromName(PREDICTIONS_CHANNEL_V1, STREAMER_ID, ACCESS_TOKEN)); - verify(eventListener).onEvent(new StreamerAddedEvent(tested, streamer, NOW)); + verify(eventHandler).onEvent(new StreamerAddedEvent(tested, streamer, NOW)); } } @@ -316,7 +385,7 @@ void addStreamerWithRaid(){ when(streamerSettings.isFollowRaid()).thenReturn(true); - tested.addEventListener(eventListener); + tested.addEventHandler(eventHandler); tested.start(); var streamer = mock(Streamer.class); @@ -333,7 +402,7 @@ void addStreamerWithRaid(){ verify(updateStreamInfo).run(streamer); verify(webSocketPool).listenTopic(Topics.buildFromName(VIDEO_PLAYBACK_BY_ID, STREAMER_ID, ACCESS_TOKEN)); verify(webSocketPool).listenTopic(Topics.buildFromName(RAID, STREAMER_ID, ACCESS_TOKEN)); - verify(eventListener).onEvent(new StreamerAddedEvent(tested, streamer, NOW)); + verify(eventHandler).onEvent(new StreamerAddedEvent(tested, streamer, NOW)); } } @@ -345,7 +414,7 @@ void addStreamerWithIrcAndStreamerOffline(){ var ircFactory = mockStatic(TwitchChatFactory.class)){ apiFactory.when(ApiFactory::createTwitchApi).thenReturn(twitchApi); apiFactory.when(() -> ApiFactory.createGqlApi(twitchLogin)).thenReturn(gqlApi); - ircFactory.when(() -> TwitchChatFactory.createChat(CHAT_MODE, twitchLogin)).thenReturn(twitchChatClient); + ircFactory.when(() -> TwitchChatFactory.createChat(tested, CHAT_MODE, false)).thenReturn(twitchChatClient); runnableFactory.when(() -> MinerRunnableFactory.createUpdateStreamInfo(tested)).thenReturn(updateStreamInfo); @@ -353,7 +422,7 @@ void addStreamerWithIrcAndStreamerOffline(){ lenient().when(streamerSettings.isJoinIrc()).thenReturn(true); - tested.addEventListener(eventListener); + tested.addEventHandler(eventHandler); tested.start(); var streamer = mock(Streamer.class); @@ -368,7 +437,7 @@ void addStreamerWithIrcAndStreamerOffline(){ verify(updateStreamInfo).run(streamer); verify(webSocketPool).listenTopic(Topics.buildFromName(VIDEO_PLAYBACK_BY_ID, STREAMER_ID, ACCESS_TOKEN)); - verify(eventListener).onEvent(new StreamerAddedEvent(tested, streamer, NOW)); + verify(eventHandler).onEvent(new StreamerAddedEvent(tested, streamer, NOW)); verify(twitchChatClient, never()).join(any()); } } @@ -381,7 +450,7 @@ void addStreamerWithIrcAndStreamerOnline(){ var ircFactory = mockStatic(TwitchChatFactory.class)){ apiFactory.when(ApiFactory::createTwitchApi).thenReturn(twitchApi); apiFactory.when(() -> ApiFactory.createGqlApi(twitchLogin)).thenReturn(gqlApi); - ircFactory.when(() -> TwitchChatFactory.createChat(CHAT_MODE, twitchLogin)).thenReturn(twitchChatClient); + ircFactory.when(() -> TwitchChatFactory.createChat(tested, CHAT_MODE, false)).thenReturn(twitchChatClient); runnableFactory.when(() -> MinerRunnableFactory.createUpdateStreamInfo(tested)).thenReturn(updateStreamInfo); @@ -389,7 +458,7 @@ void addStreamerWithIrcAndStreamerOnline(){ lenient().when(streamerSettings.isJoinIrc()).thenReturn(true); - tested.addEventListener(eventListener); + tested.addEventHandler(eventHandler); tested.start(); var streamer = mock(Streamer.class); @@ -405,7 +474,7 @@ void addStreamerWithIrcAndStreamerOnline(){ verify(updateStreamInfo).run(streamer); verify(webSocketPool).listenTopic(Topics.buildFromName(VIDEO_PLAYBACK_BY_ID, STREAMER_ID, ACCESS_TOKEN)); - verify(eventListener).onEvent(new StreamerAddedEvent(tested, streamer, NOW)); + verify(eventHandler).onEvent(new StreamerAddedEvent(tested, streamer, NOW)); verify(twitchChatClient).join(STREAMER_USERNAME); } } @@ -422,7 +491,7 @@ void addDuplicateStreamer(){ timeFactory.when(TimeFactory::now).thenReturn(NOW); - tested.addEventListener(eventListener); + tested.addEventHandler(eventHandler); tested.start(); var streamer = mock(Streamer.class); @@ -439,7 +508,7 @@ void addDuplicateStreamer(){ verify(updateStreamInfo).run(streamer); verify(webSocketPool).listenTopic(Topics.buildFromName(VIDEO_PLAYBACK_BY_ID, STREAMER_ID, ACCESS_TOKEN)); - verify(eventListener).onEvent(new StreamerAddedEvent(tested, streamer, NOW)); + verify(eventHandler).onEvent(new StreamerAddedEvent(tested, streamer, NOW)); } } @@ -471,7 +540,7 @@ void getStreamerById(){ var ircFactory = mockStatic(TwitchChatFactory.class)){ apiFactory.when(ApiFactory::createTwitchApi).thenReturn(twitchApi); apiFactory.when(() -> ApiFactory.createGqlApi(twitchLogin)).thenReturn(gqlApi); - ircFactory.when(() -> TwitchChatFactory.createChat(CHAT_MODE, twitchLogin)).thenReturn(twitchChatClient); + ircFactory.when(() -> TwitchChatFactory.createChat(tested, CHAT_MODE, false)).thenReturn(twitchChatClient); runnableFactory.when(() -> MinerRunnableFactory.createUpdateStreamInfo(tested)).thenReturn(updateStreamInfo); @@ -504,7 +573,7 @@ void removeStreamer(){ var ircFactory = mockStatic(TwitchChatFactory.class)){ apiFactory.when(ApiFactory::createTwitchApi).thenReturn(twitchApi); apiFactory.when(() -> ApiFactory.createGqlApi(twitchLogin)).thenReturn(gqlApi); - ircFactory.when(() -> TwitchChatFactory.createChat(CHAT_MODE, twitchLogin)).thenReturn(twitchChatClient); + ircFactory.when(() -> TwitchChatFactory.createChat(tested, CHAT_MODE, false)).thenReturn(twitchChatClient); runnableFactory.when(() -> MinerRunnableFactory.createUpdateStreamInfo(tested)).thenReturn(updateStreamInfo); @@ -514,7 +583,7 @@ void removeStreamer(){ when(streamer.getId()).thenReturn(STREAMER_ID); when(streamer.getUsername()).thenReturn(STREAMER_USERNAME); tested.getStreamerMap().put(STREAMER_ID, streamer); - tested.addEventListener(eventListener); + tested.addEventHandler(eventHandler); tested.start(); tested.removeStreamer(streamer); @@ -523,7 +592,7 @@ void removeStreamer(){ verify(webSocketPool).removeTopic(Topic.builder().name(PREDICTIONS_CHANNEL_V1).target(STREAMER_ID).build()); verify(webSocketPool).removeTopic(Topic.builder().name(RAID).target(STREAMER_ID).build()); verify(twitchChatClient).leave(STREAMER_USERNAME); - verify(eventListener).onEvent(new StreamerRemovedEvent(tested, streamer, NOW)); + verify(eventHandler).onEvent(new StreamerRemovedEvent(tested, streamer, NOW)); } } @@ -534,11 +603,11 @@ void removeUnknownStreamer(){ var ircFactory = mockStatic(TwitchChatFactory.class)){ apiFactory.when(ApiFactory::createTwitchApi).thenReturn(twitchApi); apiFactory.when(() -> ApiFactory.createGqlApi(twitchLogin)).thenReturn(gqlApi); - ircFactory.when(() -> TwitchChatFactory.createChat(CHAT_MODE, twitchLogin)).thenReturn(twitchChatClient); + ircFactory.when(() -> TwitchChatFactory.createChat(tested, CHAT_MODE, false)).thenReturn(twitchChatClient); runnableFactory.when(() -> MinerRunnableFactory.createUpdateStreamInfo(tested)).thenReturn(updateStreamInfo); - tested.addEventListener(eventListener); + tested.addEventHandler(eventHandler); var streamer = mock(Streamer.class); when(streamer.getId()).thenReturn(STREAMER_ID); @@ -549,7 +618,7 @@ void removeUnknownStreamer(){ verify(webSocketPool, never()).removeTopic(any()); verify(twitchChatClient, never()).leave(any()); - verify(eventListener, never()).onEvent(any()); + verify(eventHandler, never()).onEvent(any()); } } @@ -572,7 +641,7 @@ void updateStreamerAllActivatedOnline(){ var ircFactory = mockStatic(TwitchChatFactory.class)){ apiFactory.when(ApiFactory::createTwitchApi).thenReturn(twitchApi); apiFactory.when(() -> ApiFactory.createGqlApi(twitchLogin)).thenReturn(gqlApi); - ircFactory.when(() -> TwitchChatFactory.createChat(CHAT_MODE, twitchLogin)).thenReturn(twitchChatClient); + ircFactory.when(() -> TwitchChatFactory.createChat(tested, CHAT_MODE, false)).thenReturn(twitchChatClient); runnableFactory.when(() -> MinerRunnableFactory.createUpdateStreamInfo(tested)).thenReturn(updateStreamInfo); @@ -607,7 +676,7 @@ void updateStreamerAllActivatedOffline(){ var ircFactory = mockStatic(TwitchChatFactory.class)){ apiFactory.when(ApiFactory::createTwitchApi).thenReturn(twitchApi); apiFactory.when(() -> ApiFactory.createGqlApi(twitchLogin)).thenReturn(gqlApi); - ircFactory.when(() -> TwitchChatFactory.createChat(CHAT_MODE, twitchLogin)).thenReturn(twitchChatClient); + ircFactory.when(() -> TwitchChatFactory.createChat(tested, CHAT_MODE, false)).thenReturn(twitchChatClient); runnableFactory.when(() -> MinerRunnableFactory.createUpdateStreamInfo(tested)).thenReturn(updateStreamInfo); @@ -642,7 +711,7 @@ void updateStreamerNothingActivatedOnline(){ var ircFactory = mockStatic(TwitchChatFactory.class)){ apiFactory.when(ApiFactory::createTwitchApi).thenReturn(twitchApi); apiFactory.when(() -> ApiFactory.createGqlApi(twitchLogin)).thenReturn(gqlApi); - ircFactory.when(() -> TwitchChatFactory.createChat(CHAT_MODE, twitchLogin)).thenReturn(twitchChatClient); + ircFactory.when(() -> TwitchChatFactory.createChat(tested, CHAT_MODE, false)).thenReturn(twitchChatClient); runnableFactory.when(() -> MinerRunnableFactory.createUpdateStreamInfo(tested)).thenReturn(updateStreamInfo); @@ -675,7 +744,7 @@ void updateStreamerNothingActivatedOffline(){ var ircFactory = mockStatic(TwitchChatFactory.class)){ apiFactory.when(ApiFactory::createTwitchApi).thenReturn(twitchApi); apiFactory.when(() -> ApiFactory.createGqlApi(twitchLogin)).thenReturn(gqlApi); - ircFactory.when(() -> TwitchChatFactory.createChat(CHAT_MODE, twitchLogin)).thenReturn(twitchChatClient); + ircFactory.when(() -> TwitchChatFactory.createChat(tested, CHAT_MODE, false)).thenReturn(twitchChatClient); runnableFactory.when(() -> MinerRunnableFactory.createUpdateStreamInfo(tested)).thenReturn(updateStreamInfo); @@ -711,18 +780,18 @@ void getUsername(){ @Test void eventHandlers(){ - var listener1 = mock(IEventListener.class); - var listener2 = mock(IEventListener.class); + var handler1 = mock(IEventHandler.class); + var handler2 = mock(IEventHandler.class); - tested.addEventListener(listener1); - tested.addEventListener(listener2); + tested.addEventHandler(handler1); + tested.addEventHandler(handler2); var event = mock(IEvent.class); assertDoesNotThrow(() -> tested.onEvent(event)); verify(executorService, times(2)).submit(any(Runnable.class)); - verify(listener1).onEvent(event); - verify(listener2).onEvent(event); + verify(handler1).onEvent(event); + verify(handler2).onEvent(event); } @Test @@ -732,7 +801,7 @@ void requestInventorySync(){ var ircFactory = mockStatic(TwitchChatFactory.class)){ apiFactory.when(ApiFactory::createTwitchApi).thenReturn(twitchApi); apiFactory.when(() -> ApiFactory.createGqlApi(twitchLogin)).thenReturn(gqlApi); - ircFactory.when(() -> TwitchChatFactory.createChat(CHAT_MODE, twitchLogin)).thenReturn(twitchChatClient); + ircFactory.when(() -> TwitchChatFactory.createChat(tested, CHAT_MODE, false)).thenReturn(twitchChatClient); runnableFactory.when(() -> MinerRunnableFactory.createUpdateStreamInfo(tested)).thenReturn(updateStreamInfo); runnableFactory.when(() -> MinerRunnableFactory.createSyncInventory(tested)).thenReturn(syncInventory); diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/BetPlacerTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/BetPlacerTest.java index e7ed3553..a19ef6a4 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/BetPlacerTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/BetPlacerTest.java @@ -9,6 +9,7 @@ import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.EventStatus; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.factory.TransactionIdFactory; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.handler.data.PredictionState; @@ -60,6 +61,8 @@ class BetPlacerTest{ @Mock private GQLApi gqlApi; @Mock + private IDatabase database; + @Mock private BettingPrediction bettingPrediction; @Mock private Event event; @@ -87,6 +90,7 @@ class BetPlacerTest{ @BeforeEach void setUp() throws BetPlacementException{ lenient().when(miner.getGqlApi()).thenReturn(gqlApi); + lenient().when(miner.getDatabase()).thenReturn(database); lenient().when(bettingPrediction.getEvent()).thenReturn(event); lenient().when(bettingPrediction.getStreamer()).thenReturn(streamer); @@ -101,7 +105,7 @@ void setUp() throws BetPlacementException{ lenient().when(outcome.getId()).thenReturn(OUTCOME_ID); - lenient().when(outcomePicker.chooseOutcome(bettingPrediction)).thenReturn(outcome); + lenient().when(outcomePicker.chooseOutcome(bettingPrediction, database)).thenReturn(outcome); lenient().when(amountCalculator.calculateAmount(bettingPrediction, outcome)).thenReturn(AMOUNT); lenient().when(gqlApi.makePrediction(EVENT_ID, OUTCOME_ID, AMOUNT, TRANSACTION_ID)).thenReturn(Optional.of(gqlResponse)); @@ -121,7 +125,7 @@ void eventNotActive(){ @Test void outcomeException() throws BetPlacementException{ - when(outcomePicker.chooseOutcome(bettingPrediction)).thenThrow(new BetPlacementException("For tests")); + when(outcomePicker.chooseOutcome(bettingPrediction, database)).thenThrow(new BetPlacementException("For tests")); assertDoesNotThrow(() -> tested.placeBet(bettingPrediction)); @@ -131,7 +135,7 @@ void outcomeException() throws BetPlacementException{ @Test void outcomeException2() throws BetPlacementException{ - when(outcomePicker.chooseOutcome(bettingPrediction)).thenThrow(new NotEnoughUsersBetPlacementException(0)); + when(outcomePicker.chooseOutcome(bettingPrediction, database)).thenThrow(new NotEnoughUsersBetPlacementException(0)); assertDoesNotThrow(() -> tested.placeBet(bettingPrediction)); diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/BiggestPredictorOutcomePickerTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/BiggestPredictorOutcomePickerTest.java index eb123c12..bde14665 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/BiggestPredictorOutcomePickerTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/BiggestPredictorOutcomePickerTest.java @@ -3,6 +3,7 @@ import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Predictor; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; import fr.raksrinana.channelpointsminer.miner.tests.ParallelizableTest; @@ -23,6 +24,8 @@ class BiggestPredictorOutcomePickerTest{ private final BiggestPredictorOutcomePicker tested = BiggestPredictorOutcomePicker.builder().build(); + @Mock + private IDatabase database; @Mock private BettingPrediction bettingPrediction; @Mock @@ -53,13 +56,13 @@ void chose() throws BetPlacementException{ when(blueOutcome.getTopPredictors()).thenReturn(List.of(predictor10, predictor11)); when(pinkOutcome.getTopPredictors()).thenReturn(List.of(predictor20, predictor21)); - assertThat(tested.chooseOutcome(bettingPrediction)).isEqualTo(pinkOutcome); + assertThat(tested.chooseOutcome(bettingPrediction, database)).isEqualTo(pinkOutcome); } @Test void missingOutcome(){ when(event.getOutcomes()).thenReturn(List.of()); - assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction)); + assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction, database)); } } \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastPointsOutcomePickerTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastPointsOutcomePickerTest.java index ab101515..d1adc871 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastPointsOutcomePickerTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastPointsOutcomePickerTest.java @@ -2,6 +2,7 @@ import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; import fr.raksrinana.channelpointsminer.miner.tests.ParallelizableTest; @@ -21,6 +22,8 @@ class LeastPointsOutcomePickerTest{ private final LeastPointsOutcomePicker tested = LeastPointsOutcomePicker.builder().build(); + @Mock + private IDatabase database; @Mock private BettingPrediction bettingPrediction; @Mock @@ -41,13 +44,13 @@ void chose() throws BetPlacementException{ when(blueOutcome.getTotalPoints()).thenReturn(20L); when(pinkOutcome.getTotalPoints()).thenReturn(19L); - assertThat(tested.chooseOutcome(bettingPrediction)).isEqualTo(pinkOutcome); + assertThat(tested.chooseOutcome(bettingPrediction, database)).isEqualTo(pinkOutcome); } @Test void missingOutcome(){ when(event.getOutcomes()).thenReturn(List.of()); - assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction)); + assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction, database)); } } \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastUsersOutcomePickerTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastUsersOutcomePickerTest.java index 5d70f70c..b68c0bd2 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastUsersOutcomePickerTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/LeastUsersOutcomePickerTest.java @@ -2,6 +2,7 @@ import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; import fr.raksrinana.channelpointsminer.miner.tests.ParallelizableTest; @@ -21,6 +22,8 @@ class LeastUsersOutcomePickerTest{ private final LeastUsersOutcomePicker tested = LeastUsersOutcomePicker.builder().build(); + @Mock + private IDatabase database; @Mock private BettingPrediction bettingPrediction; @Mock @@ -41,13 +44,13 @@ void chose() throws BetPlacementException{ when(blueOutcome.getTotalUsers()).thenReturn(20); when(pinkOutcome.getTotalUsers()).thenReturn(19); - assertThat(tested.chooseOutcome(bettingPrediction)).isEqualTo(pinkOutcome); + assertThat(tested.chooseOutcome(bettingPrediction, database)).isEqualTo(pinkOutcome); } @Test void missingOutcome(){ when(event.getOutcomes()).thenReturn(List.of()); - assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction)); + assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction, database)); } } \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostPointsOutcomePickerTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostPointsOutcomePickerTest.java index e2464ae2..50af3d7f 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostPointsOutcomePickerTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostPointsOutcomePickerTest.java @@ -2,6 +2,7 @@ import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; import fr.raksrinana.channelpointsminer.miner.tests.ParallelizableTest; @@ -21,6 +22,8 @@ class MostPointsOutcomePickerTest{ private final MostPointsOutcomePicker tested = MostPointsOutcomePicker.builder().build(); + @Mock + private IDatabase database; @Mock private BettingPrediction bettingPrediction; @Mock @@ -41,13 +44,13 @@ void chose() throws BetPlacementException{ when(blueOutcome.getTotalPoints()).thenReturn(19L); when(pinkOutcome.getTotalPoints()).thenReturn(20L); - assertThat(tested.chooseOutcome(bettingPrediction)).isEqualTo(pinkOutcome); + assertThat(tested.chooseOutcome(bettingPrediction, database)).isEqualTo(pinkOutcome); } @Test void missingOutcome(){ when(event.getOutcomes()).thenReturn(List.of()); - assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction)); + assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction, database)); } } \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostTrustedPickerTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostTrustedPickerTest.java new file mode 100644 index 00000000..0a72158f --- /dev/null +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostTrustedPickerTest.java @@ -0,0 +1,138 @@ +package fr.raksrinana.channelpointsminer.miner.prediction.bet.outcome; + +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Badge; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; +import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; +import fr.raksrinana.channelpointsminer.miner.database.model.prediction.OutcomeStatistic; +import fr.raksrinana.channelpointsminer.miner.factory.DatabaseFactory; +import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; +import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; +import fr.raksrinana.channelpointsminer.miner.tests.ParallelizableTest; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import java.sql.SQLException; +import java.util.Collections; +import java.util.List; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +@SuppressWarnings("ResultOfMethodCallIgnored") +@ParallelizableTest +@ExtendWith(MockitoExtension.class) +class MostTrustedPickerTest{ + + private final static int MIN_TOTAL_BETS_PLACED_BY_USER = 5; + private final static int MIN_TOTAL_BETS_PLACED_ON_PREDICTION = 10; + private final static int MIN_TOTAL_BETS_PLACED_ON_OUTCOME = 5; + private final static String BADGE_1 = "blue-1"; + private final static String BADGE_2 = "pink-2"; + + private final static String CHANNEL_ID = "channelid"; + + private final MostTrustedPicker tested = MostTrustedPicker.builder() + .minTotalBetsPlacedByUser(MIN_TOTAL_BETS_PLACED_BY_USER) + .minTotalBetsPlacedOnPrediction(MIN_TOTAL_BETS_PLACED_ON_PREDICTION) + .minTotalBetsPlacedOnOutcome(MIN_TOTAL_BETS_PLACED_ON_OUTCOME) + .build(); + + @Mock + private IDatabase database; + @Mock + private BettingPrediction bettingPrediction; + @Mock + private Event event; + @Mock + private Outcome blueOutcome; + @Mock + private Outcome pinkOutcome; + @Mock + private Badge blueBadge; + @Mock + private Badge pinkBadge; + @Mock + private OutcomeStatistic outcomeStatisticBlue; + @Mock + private OutcomeStatistic outcomeStatisticPink; + + @BeforeEach + void setUp() throws SQLException{ + lenient().when(bettingPrediction.getEvent()).thenReturn(event); + lenient().when(event.getOutcomes()).thenReturn(List.of(blueOutcome, pinkOutcome)); + lenient().when(event.getTitle()).thenReturn("title"); + lenient().when(event.getChannelId()).thenReturn(CHANNEL_ID); + + lenient().when(blueOutcome.getTitle()).thenReturn("blue"); + lenient().when(pinkOutcome.getTitle()).thenReturn("pink"); + + lenient().when(blueOutcome.getBadge()).thenReturn(blueBadge); + lenient().when(pinkOutcome.getBadge()).thenReturn(pinkBadge); + lenient().when(blueBadge.getVersion()).thenReturn(BADGE_1); + lenient().when(pinkBadge.getVersion()).thenReturn(BADGE_2); + + lenient().when(outcomeStatisticBlue.getBadge()).thenReturn(BADGE_1); + lenient().when(outcomeStatisticPink.getBadge()).thenReturn(BADGE_2); + + lenient().when(outcomeStatisticBlue.getAverageReturnOnInvestment()).thenReturn(1.1); + lenient().when(outcomeStatisticPink.getAverageReturnOnInvestment()).thenReturn(1.0); + + lenient().when(outcomeStatisticBlue.getUserCnt()).thenReturn(10); + lenient().when(outcomeStatisticPink.getUserCnt()).thenReturn(10); + + lenient().when(database.getOutcomeStatisticsForChannel(CHANNEL_ID, MIN_TOTAL_BETS_PLACED_BY_USER)).thenReturn(List.of(outcomeStatisticBlue, outcomeStatisticPink)); + } + + @Test + void chooseOutcome(){ + try(var databaseFactory = mockStatic(DatabaseFactory.class)){ + Outcome outcome = assertDoesNotThrow(() -> tested.chooseOutcome(bettingPrediction, database)); + + assertEquals(BADGE_1, outcome.getBadge().getVersion()); + } + } + + @Test + void notEnoughTotalBetsPlaced(){ + try(var databaseFactory = mockStatic(DatabaseFactory.class)){ + lenient().when(outcomeStatisticBlue.getUserCnt()).thenReturn(6); + lenient().when(outcomeStatisticPink.getUserCnt()).thenReturn(2); + + assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction, database)); + } + } + + @Test + void notEnoughBetsPlacedOnOutcome(){ + try(var databaseFactory = mockStatic(DatabaseFactory.class)){ + lenient().when(outcomeStatisticBlue.getUserCnt()).thenReturn(2); + lenient().when(outcomeStatisticPink.getUserCnt()).thenReturn(20); + + assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction, database)); + } + } + + @Test + void databaseThrowsException() throws SQLException{ + try(var databaseFactory = mockStatic(DatabaseFactory.class)){ + when(database.getOutcomeStatisticsForChannel(CHANNEL_ID, MIN_TOTAL_BETS_PLACED_BY_USER)).thenThrow(new SQLException("")); + + assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction, database)); + } + } + + @Test + void emptyStatistics() throws SQLException{ + try(var databaseFactory = mockStatic(DatabaseFactory.class)){ + when(database.getOutcomeStatisticsForChannel(CHANNEL_ID, MIN_TOTAL_BETS_PLACED_BY_USER)).thenReturn(Collections.emptyList()); + + assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction, database)); + } + } +} \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostUsersOutcomePickerTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostUsersOutcomePickerTest.java index c34ab7f2..4d7dd823 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostUsersOutcomePickerTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/MostUsersOutcomePickerTest.java @@ -2,6 +2,7 @@ import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; import fr.raksrinana.channelpointsminer.miner.tests.ParallelizableTest; @@ -21,6 +22,8 @@ class MostUsersOutcomePickerTest{ private final MostUsersOutcomePicker tested = MostUsersOutcomePicker.builder().build(); + @Mock + private IDatabase database; @Mock private BettingPrediction bettingPrediction; @Mock @@ -41,13 +44,13 @@ void chose() throws BetPlacementException{ when(blueOutcome.getTotalUsers()).thenReturn(19); when(pinkOutcome.getTotalUsers()).thenReturn(20); - assertThat(tested.chooseOutcome(bettingPrediction)).isEqualTo(pinkOutcome); + assertThat(tested.chooseOutcome(bettingPrediction, database)).isEqualTo(pinkOutcome); } @Test void missingOutcome(){ when(event.getOutcomes()).thenReturn(List.of()); - assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction)); + assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction, database)); } } \ No newline at end of file diff --git a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/SmartOutcomePickerTest.java b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/SmartOutcomePickerTest.java index f0ed6f1c..76f83654 100644 --- a/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/SmartOutcomePickerTest.java +++ b/miner/src/test/java/fr/raksrinana/channelpointsminer/miner/prediction/bet/outcome/SmartOutcomePickerTest.java @@ -3,6 +3,7 @@ import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Event; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.Outcome; import fr.raksrinana.channelpointsminer.miner.api.ws.data.message.subtype.OutcomeColor; +import fr.raksrinana.channelpointsminer.miner.database.IDatabase; import fr.raksrinana.channelpointsminer.miner.handler.data.BettingPrediction; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.BetPlacementException; import fr.raksrinana.channelpointsminer.miner.prediction.bet.exception.NotEnoughUsersBetPlacementException; @@ -23,6 +24,8 @@ class SmartOutcomePickerTest{ private final SmartOutcomePicker tested = SmartOutcomePicker.builder().percentageGap(.1F).build(); + @Mock + private IDatabase database; @Mock private BettingPrediction bettingPrediction; @Mock @@ -51,7 +54,7 @@ void choseByPoints() throws BetPlacementException{ when(blueOutcome.getTotalPoints()).thenReturn(10L); when(pinkOutcome.getTotalPoints()).thenReturn(20L); - assertThat(tested.chooseOutcome(bettingPrediction)).isEqualTo(blueOutcome); + assertThat(tested.chooseOutcome(bettingPrediction, database)).isEqualTo(blueOutcome); } @Test @@ -65,7 +68,7 @@ void choseByPointsMoreOutcomes() throws BetPlacementException{ when(pinkOutcome.getTotalPoints()).thenReturn(20L); when(redOutcome.getTotalPoints()).thenReturn(19L); - assertThat(tested.chooseOutcome(bettingPrediction)).isEqualTo(redOutcome); + assertThat(tested.chooseOutcome(bettingPrediction, database)).isEqualTo(redOutcome); } @Test @@ -73,7 +76,7 @@ void choseByUsers() throws BetPlacementException{ when(blueOutcome.getTotalUsers()).thenReturn(40); when(pinkOutcome.getTotalUsers()).thenReturn(60); - assertThat(tested.chooseOutcome(bettingPrediction)).isEqualTo(pinkOutcome); + assertThat(tested.chooseOutcome(bettingPrediction, database)).isEqualTo(pinkOutcome); } @Test @@ -81,7 +84,7 @@ void missingBlue(){ when(event.getOutcomes()).thenReturn(List.of(pinkOutcome)); when(pinkOutcome.getTotalUsers()).thenReturn(1); - assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction)); + assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction, database)); } @Test @@ -89,7 +92,7 @@ void missingPink(){ when(event.getOutcomes()).thenReturn(List.of(blueOutcome)); when(blueOutcome.getTotalUsers()).thenReturn(1); - assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction)); + assertThrows(BetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction, database)); } @Test @@ -97,6 +100,6 @@ void noUsers() throws BetPlacementException{ when(blueOutcome.getTotalUsers()).thenReturn(0); when(pinkOutcome.getTotalUsers()).thenReturn(0); - assertThrows(NotEnoughUsersBetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction)); + assertThrows(NotEnoughUsersBetPlacementException.class, () -> tested.chooseOutcome(bettingPrediction, database)); } } \ No newline at end of file diff --git a/miner/src/test/resources/config/config-with-more-customization.json b/miner/src/test/resources/config/config-with-more-customization.json index c4046a0b..de4027df 100644 --- a/miner/src/test/resources/config/config-with-more-customization.json +++ b/miner/src/test/resources/config/config-with-more-customization.json @@ -23,11 +23,13 @@ }, "reloadEvery": 15, "analytics": { - "enabled": true, - "database": { - "jdbcUrl": "jdbcUrl", - "username": "user", - "password": "pass" + "enabled" : true, + "recordChatsPredictions" : true, + "database" : { + "jdbcUrl" : "jdbcUrl", + "username" : "user", + "password" : "pass", + "maxPoolSize" : 15 } } }