Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: common websocket client (#552)
* feat: add websocket client module and use it in twitch chat * feat: use new websocket-client module in pubsub * chore: keep the dependency on common for client-websocket for now * fix: apply pr feedback * chore: use proxyConfig in websocket-common * fix: honor ExponentialBackoffStrategy maxRetries * fix: update tests for the latest changes * feat: move some util classes into a util module / add IBackoffStrategy * refactor: make IBackoffStrategy#sleep default * chore: switch nv-websocket-client dep from api to implementation Co-authored-by: Sidd <iProdigy@users.noreply.github.com> * feat: allow setting headers for the ws connection * feat: customizable websocket connection backoff strategy for chat/pubsub * fix: set default of ws conn retry immediateFirst to false Co-authored-by: Sidd <iProdigy@users.noreply.github.com> Co-authored-by: Sidd <iProdigy@users.noreply.github.com>
- Loading branch information
1 parent
91788a0
commit 8e0c28f
Showing
29 changed files
with
1,622 additions
and
985 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
408 changes: 152 additions & 256 deletions
408
chat/src/main/java/com/github/twitch4j/chat/TwitchChat.java
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 5 additions & 8 deletions
13
chat/src/main/java/com/github/twitch4j/chat/enums/TMIConnectionState.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,9 @@ | ||
package com.github.twitch4j.chat.enums; | ||
|
||
/** | ||
* WebSocket - Connection State | ||
*/ | ||
public enum TMIConnectionState { | ||
DISCONNECTING, | ||
RECONNECTING, | ||
DISCONNECTED, | ||
CONNECTING, | ||
CONNECTED | ||
DISCONNECTING, | ||
RECONNECTING, | ||
DISCONNECTED, | ||
CONNECTING, | ||
CONNECTED | ||
} |
88 changes: 88 additions & 0 deletions
88
chat/src/test/java/com/github/twitch4j/chat/TwitchChatRateLimitTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package com.github.twitch4j.chat; | ||
|
||
import com.github.twitch4j.chat.util.TestUtils; | ||
import com.github.twitch4j.client.websocket.WebsocketConnection; | ||
import com.github.twitch4j.client.websocket.domain.WebsocketConnectionState; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.junit.jupiter.api.Assertions; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.mockito.Mockito; | ||
|
||
import java.time.Duration; | ||
|
||
import static org.awaitility.Awaitility.await; | ||
import static org.mockito.ArgumentMatchers.eq; | ||
import static org.mockito.ArgumentMatchers.startsWith; | ||
import static org.mockito.Mockito.times; | ||
|
||
@Slf4j | ||
public class TwitchChatRateLimitTest { | ||
|
||
private WebsocketConnection connection; | ||
|
||
private TwitchChat twitchChat; | ||
|
||
@BeforeEach | ||
void init() { | ||
// mock connection | ||
connection = Mockito.mock(WebsocketConnection.class); | ||
|
||
// construct twitchChat | ||
twitchChat = TwitchChatBuilder.builder() | ||
.withWebsocketConnection(connection) | ||
.withChatAccount(TestUtils.getCredential()) | ||
.withCommandTrigger("!") | ||
.build(); | ||
} | ||
|
||
@Test | ||
public void testJoinRateLimit() { | ||
// fake connected state | ||
Mockito.when(connection.getConnectionState()).thenReturn(WebsocketConnectionState.CONNECTED); | ||
|
||
// join a bunch of channels | ||
for (int i = 1; i <= 100; i++) { | ||
twitchChat.joinChannel("twitch4j" + i); | ||
} | ||
|
||
// wait | ||
await().atMost(Duration.ofSeconds(1)).until(() -> twitchChat.ircCommandQueue.size() <= 80); | ||
|
||
// verify | ||
Mockito.verify(connection, times(20)).sendText(startsWith("JOIN #twitch4j")); | ||
} | ||
|
||
@Test | ||
public void testChannelMessageRateLimit() { | ||
// fake connected state | ||
Mockito.when(connection.getConnectionState()).thenReturn(WebsocketConnectionState.CONNECTED); | ||
|
||
// join a bunch of messages | ||
for (int i = 1; i <= 100; i++) { | ||
twitchChat.sendMessage("twitch4j", "Hello @twitch4j"); | ||
} | ||
|
||
// wait for all commands to be processed | ||
await().atMost(Duration.ofSeconds(1)).until(() -> twitchChat.ircCommandQueue.size() <= 80); | ||
|
||
// verify | ||
Assertions.assertEquals(0, twitchChat.ircCommandQueue.size(), "there shouldn't be any queued messages left"); | ||
Mockito.verify(connection, times(20)).sendText(eq("PRIVMSG #twitch4j :Hello @twitch4j")); | ||
} | ||
|
||
@Test | ||
public void testAuthRateLimit() { | ||
// fake disconnected state | ||
Mockito.when(connection.getConnectionState()).thenReturn(WebsocketConnectionState.DISCONNECTED); | ||
|
||
// connect a few times | ||
for (int i = 1; i <= 20; i++) { | ||
twitchChat.connect(); | ||
} | ||
|
||
// verify | ||
Assertions.assertEquals(0, twitchChat.ircAuthBucket.getAvailableTokens(), "we should have used up all auth tokens"); | ||
} | ||
|
||
} |
127 changes: 71 additions & 56 deletions
127
chat/src/test/java/com/github/twitch4j/chat/TwitchChatTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,86 +1,101 @@ | ||
package com.github.twitch4j.chat; | ||
|
||
import com.github.philippheuer.credentialmanager.CredentialManager; | ||
import com.github.philippheuer.credentialmanager.CredentialManagerBuilder; | ||
import com.github.philippheuer.events4j.core.EventManager; | ||
import com.github.twitch4j.auth.providers.TwitchIdentityProvider; | ||
import com.github.twitch4j.chat.events.CommandEvent; | ||
import com.github.twitch4j.chat.events.channel.ChannelMessageEvent; | ||
import com.github.twitch4j.chat.events.channel.IRCMessageEvent; | ||
import com.github.twitch4j.chat.util.TestUtils; | ||
import com.github.twitch4j.client.websocket.WebsocketConnection; | ||
import com.github.twitch4j.client.websocket.domain.WebsocketConnectionState; | ||
import com.github.twitch4j.common.enums.CommandPermission; | ||
import com.github.twitch4j.common.test.TestEventManager; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.junit.jupiter.api.*; | ||
import org.junit.jupiter.api.Assertions; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.mockito.Mockito; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.time.Duration; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
import static org.awaitility.Awaitility.await; | ||
import static org.mockito.ArgumentMatchers.eq; | ||
import static org.mockito.Mockito.times; | ||
|
||
@Slf4j | ||
@Tag("integration") | ||
public class TwitchChatTest { | ||
|
||
private static TwitchChat twitchChat; | ||
private WebsocketConnection connection; | ||
|
||
@BeforeAll | ||
public static void connectToChat() { | ||
// event manager | ||
EventManager eventManager = new EventManager(); | ||
eventManager.autoDiscovery(); | ||
private TestEventManager eventManager; | ||
|
||
// credential manager | ||
CredentialManager credentialManager = CredentialManagerBuilder.builder().build(); | ||
credentialManager.registerIdentityProvider(new TwitchIdentityProvider("jzkbprff40iqj646a697cyrvl0zt2m6", "**SECRET**", "")); | ||
private TwitchChat twitchChat; | ||
|
||
@BeforeEach | ||
void init() { | ||
// mock connection | ||
connection = Mockito.mock(WebsocketConnection.class); | ||
|
||
// mock eventManager | ||
eventManager = new TestEventManager(); | ||
|
||
// construct twitchChat | ||
twitchChat = TwitchChatBuilder.builder() | ||
.withEventManager(eventManager) | ||
.withCredentialManager(credentialManager) | ||
.withChatAccount(TestUtils.getCredential()) | ||
.withCommandTrigger("!") | ||
.build(); | ||
|
||
// sleep for a few seconds so that we're connected | ||
TestUtils.sleepFor(5000); | ||
.withWebsocketConnection(connection) | ||
.withEventManager(eventManager) | ||
.withChatAccount(TestUtils.getCredential()) | ||
.withCommandTrigger("!") | ||
.build(); | ||
} | ||
|
||
@Test | ||
@DisplayName("Tests sending and receiving channel messages") | ||
public void sendTwitchChannelMessage() { | ||
// listen for events in channel | ||
List<ChannelMessageEvent> channelMessages = new ArrayList<>(); | ||
twitchChat.joinChannel("twitch4j"); | ||
twitchChat.getEventManager().onEvent(ChannelMessageEvent.class, event -> { | ||
channelMessages.add(event); | ||
log.debug(event.toString()); | ||
}); | ||
public void testJoinChannel() { | ||
// fake connected state | ||
Mockito.when(connection.getConnectionState()).thenReturn(WebsocketConnectionState.CONNECTED); | ||
|
||
// send message to channel | ||
twitchChat.sendMessage("twitch4j", "Hello @twitch4j"); | ||
// join channel | ||
twitchChat.joinChannel("twitch4j"); | ||
|
||
// sleep a second and look of the message was sended | ||
TestUtils.sleepFor(1000); | ||
// wait for all commands to be processed | ||
await().atMost(Duration.ofSeconds(1)).until(() -> twitchChat.ircCommandQueue.size() == 0); | ||
|
||
// check if the message was send and received | ||
assertTrue(twitchChat.ircCommandQueue.size() == 0, "Can't find the message we send in the received messages!"); | ||
// verify | ||
Assertions.assertEquals(WebsocketConnectionState.CONNECTED, connection.getConnectionState(), "should be CONNECTED"); | ||
Assertions.assertEquals(0, twitchChat.ircCommandQueue.size(), "there shouldn't be any queued messages left"); | ||
Mockito.verify(connection, times(1)).sendText(eq("JOIN #twitch4j")); | ||
} | ||
|
||
@Test | ||
@DisplayName("Local test to keep it running for debugging") | ||
@Disabled | ||
public void localTestRun() { | ||
// listen for events in channel | ||
List<ChannelMessageEvent> channelMessages = new ArrayList<>(); | ||
twitchChat.joinChannel("twitch4j"); | ||
twitchChat.getEventManager().onEvent(ChannelMessageEvent.class, event -> { | ||
log.debug(event.toString()); | ||
}); | ||
public void testSendChannelMessage() { | ||
// fake connected state | ||
Mockito.when(connection.getConnectionState()).thenReturn(WebsocketConnectionState.CONNECTED); | ||
|
||
twitchChat.getEventManager().onEvent(CommandEvent.class, event -> { | ||
log.debug(event.toString()); | ||
}); | ||
// send messages | ||
twitchChat.sendMessage("twitch4j", "Hello @twitch4j"); | ||
|
||
// wait for all commands to be processed | ||
await().atMost(Duration.ofSeconds(1)).until(() -> twitchChat.ircCommandQueue.size() == 0); | ||
|
||
// sleep a second and look of the message was sended | ||
TestUtils.sleepFor(120000); | ||
// verify | ||
Assertions.assertEquals(WebsocketConnectionState.CONNECTED, connection.getConnectionState(), "should be CONNECTED"); | ||
Assertions.assertEquals(0, twitchChat.ircCommandQueue.size(), "there shouldn't be any queued messages left"); | ||
Mockito.verify(connection, times(1)).sendText(eq("PRIVMSG #twitch4j :Hello @twitch4j")); | ||
} | ||
|
||
@Test | ||
public void testReceiveChannelMessage() { | ||
// simulate a message | ||
twitchChat.onTextMessage("@badge-info=;badges=moments/1;client-nonce=2a752cf1b27d354c11cbc1b845229091;color=#00FF7F;display-name=Twitch4J;emotes=;first-msg=0;flags=;id=7bb22cd5-4882-4d79-b12f-8c9473004542;mod=0;room-id=149223493;subscriber=0;tmi-sent-ts=1647099473133;turbo=0;user-id=149223493;user-type= :twitch4j!twitch4j@twitch4j.tmi.twitch.tv PRIVMSG #twitch4j :hello world"); | ||
|
||
// expect a IRCMessageEvent and ChannelMessageEvent | ||
Assertions.assertEquals(2, eventManager.getPublishedEvents().size()); | ||
Assertions.assertTrue(eventManager.getPublishedEvents().get(0) instanceof IRCMessageEvent); | ||
Assertions.assertTrue(eventManager.getPublishedEvents().get(1) instanceof ChannelMessageEvent); | ||
ChannelMessageEvent event = ((ChannelMessageEvent) eventManager.getPublishedEvents().get(1)); | ||
Assertions.assertEquals("2a752cf1b27d354c11cbc1b845229091", event.getNonce()); | ||
Assertions.assertEquals(false, event.isDesignatedFirstMessage()); | ||
Assertions.assertTrue(event.getPermissions().contains(CommandPermission.EVERYONE)); | ||
Assertions.assertEquals("twitch4j", event.getChannel().getName()); | ||
Assertions.assertEquals("twitch4j", event.getUser().getName()); | ||
Assertions.assertEquals("Twitch4J", event.getMessageEvent().getTags().get("display-name")); | ||
Assertions.assertEquals("hello world", event.getMessage()); | ||
} | ||
|
||
} |
Oops, something went wrong.