Skip to content

Commit

Permalink
Merge pull request #229 from RakSrinaNa/feature/ws-chat
Browse files Browse the repository at this point in the history
Connect to chat using WebSocket
  • Loading branch information
Rakambda authored Jul 21, 2022
2 parents e988323 + cb5df6d commit 490afa9
Show file tree
Hide file tree
Showing 51 changed files with 1,069 additions and 229 deletions.
9 changes: 8 additions & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Examples can be found in link:https://github.com/RakSrinaNa/ChannelPointsMiner/b

Account settings define all the settings for a Twitch account that will be used to mine points.

[cols="1,3,1"]
[cols="1,3a,1"]
|===
|Name |Description |Default value

Expand Down Expand Up @@ -176,6 +176,13 @@ NOTE: Only <<streamer_settings,streamer settings>> are reloaded, not the <<globa
|analytics
|<<analytics_settings,Analytics settings>>.
|

|chatMode
|Define what method is used to connect to Twitch chat.

* WS: Using WebSocket method (used on Twitch website).
* IRC: Using IRC protocol.
|WS
|===

=== Streamer directories [[streamer_directories]]
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ kitteh-irc-version = "8.0.0"
hikari-cp-version = "5.0.1"
mariadb-version = "3.0.6"
sqlite-version = "3.36.0.3"
rerunner-jupiter-version = "2.1.6"

shadow-version = "7.1.2"
names-version = "0.42.0"
Expand Down Expand Up @@ -57,6 +58,7 @@ mockito-junit = { group = "org.mockito", name = "mockito-junit-jupiter", version
awaitility = { group = "org.awaitility", name = "awaitility", version.ref = "awaitility-version" }
json-unit = { group = "net.javacrumbs.json-unit", name = "json-unit", version.ref = "json-unit-version" }
json-unit-assertj = { group = "net.javacrumbs.json-unit", name = "json-unit-assertj", version.ref = "json-unit-version" }
rerunnerJupiter = { group = "io.github.artsok", name = "rerunner-jupiter", version.ref = "rerunner-jupiter-version" }

[bundles]
jackson = ["jackson-core", "jackson-annotations", "jackson-databind"]
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
10 changes: 5 additions & 5 deletions gradlew
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/sh

#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -32,10 +32,10 @@
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
Expand Down
1 change: 1 addition & 0 deletions miner/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies {
testImplementation(libs.awaitility)
testImplementation(libs.unirestMocks)
testImplementation(libs.bundles.jsonUnit)
testImplementation(libs.rerunnerJupiter)
}

tasks {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package fr.raksrinana.channelpointsminer.miner.api.chat;

import org.jetbrains.annotations.NotNull;

public interface ITwitchChatClient extends AutoCloseable{
void join(@NotNull String channel);

void leave(@NotNull String channel);

@Override
void close();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package fr.raksrinana.channelpointsminer.miner.irc;
package fr.raksrinana.channelpointsminer.miner.api.chat.irc;

import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatClient;
import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
Expand All @@ -12,14 +13,15 @@

@RequiredArgsConstructor
@Log4j2
public class TwitchIrcClient implements AutoCloseable{
public class TwitchIrcChatClient implements ITwitchChatClient{

@NotNull
private final TwitchLogin twitchLogin;

@Nullable
private Client ircClient;

@Override
public void join(@NotNull String channel){
var client = getIrcClient();
var ircChannelName = "#%s".formatted(channel.toLowerCase(Locale.ROOT));
Expand All @@ -36,18 +38,19 @@ private synchronized Client getIrcClient(){
if(Objects.isNull(ircClient)){
log.info("Creating new Twitch IRC client");

ircClient = TwitchIrcFactory.createClient(twitchLogin);
ircClient = TwitchIrcFactory.createIrcClient(twitchLogin);
ircClient.connect();
ircClient.setExceptionListener(e -> log.error("Error from irc", e));

ircClient.getEventManager().registerEventListener(TwitchIrcFactory.createListener(twitchLogin.getUsername()));
ircClient.getEventManager().registerEventListener(TwitchIrcFactory.createIrcListener(twitchLogin.getUsername()));

log.info("IRC Client created");
}

return ircClient;
}

@Override
public void leave(@NotNull String channel){
if(Objects.isNull(ircClient)){
log.debug("Didn't leave irc channel #{} as no connection has been made", channel);
Expand All @@ -64,6 +67,7 @@ public void leave(@NotNull String channel){
ircClient.removeChannel(ircChannelName);
}

@Override
public void close(){
Optional.ofNullable(ircClient).ifPresent(Client::shutdown);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package fr.raksrinana.channelpointsminer.miner.irc;
package fr.raksrinana.channelpointsminer.miner.api.chat.irc;

import fr.raksrinana.channelpointsminer.miner.log.LogContext;
import lombok.RequiredArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package fr.raksrinana.channelpointsminer.miner.irc;
package fr.raksrinana.channelpointsminer.miner.api.chat.irc;

import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin;
import lombok.AccessLevel;
Expand All @@ -14,17 +14,12 @@ public class TwitchIrcFactory{
private static final String TWITCH_IRC_HOST = "irc.chat.twitch.tv";

@NotNull
public static TwitchIrcClient create(@NotNull TwitchLogin twitchLogin){
return new TwitchIrcClient(twitchLogin);
public static Client createIrcClient(@NotNull TwitchLogin twitchLogin){
return createIrcClient(twitchLogin.getUsername(), "oauth:%s".formatted(twitchLogin.getAccessToken()));
}

@NotNull
public static Client createClient(@NotNull TwitchLogin twitchLogin){
return createClient(twitchLogin.getUsername(), "oauth:%s".formatted(twitchLogin.getAccessToken()));
}

@NotNull
private static Client createClient(@NotNull String username, @Nullable String password){
private static Client createIrcClient(@NotNull String username, @Nullable String password){
var client = Client.builder()
.server()
.host(TWITCH_IRC_HOST).port(443, SECURE)
Expand All @@ -36,7 +31,7 @@ private static Client createClient(@NotNull String username, @Nullable String pa
}

@NotNull
public static TwitchIrcEventListener createListener(@NotNull String accountName){
public static TwitchIrcEventListener createIrcListener(@NotNull String accountName){
return new TwitchIrcEventListener(accountName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package fr.raksrinana.channelpointsminer.miner.api.chat.ws;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public interface ITwitchChatWebSocketListener{
void onWebSocketClosed(@NotNull TwitchChatWebSocketClient client, int code, @Nullable String reason, boolean remote);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package fr.raksrinana.channelpointsminer.miner.api.chat.ws;

import fr.raksrinana.channelpointsminer.miner.api.chat.ITwitchChatClient;
import fr.raksrinana.channelpointsminer.miner.api.passport.TwitchLogin;
import fr.raksrinana.channelpointsminer.miner.factory.TimeFactory;
import fr.raksrinana.channelpointsminer.miner.log.LogContext;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import org.java_websocket.WebSocket;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.framing.Framedata;
import org.java_websocket.handshake.ServerHandshake;
import org.jetbrains.annotations.NotNull;
import java.net.URI;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;

@Log4j2
public class TwitchChatWebSocketClient extends WebSocketClient implements ITwitchChatClient{
@Getter
private final Set<String> channels;
private final List<ITwitchChatWebSocketListener> listeners;
@Getter
private final String uuid;
private final TwitchLogin twitchLogin;

@Getter
private Instant lastPing;

public TwitchChatWebSocketClient(@NotNull URI uri, @NotNull TwitchLogin twitchLogin){
super(uri);
this.twitchLogin = twitchLogin;
uuid = UUID.randomUUID().toString();

setConnectionLostTimeout(0);
channels = new HashSet<>();
listeners = new ArrayList<>();
lastPing = Instant.EPOCH;
}

@Override
public void onOpen(ServerHandshake serverHandshake){
try(var ignored = LogContext.empty().withSocketId(uuid)){
log.info("IRC WebSocket opened");
onPing();
sendMessage("CAP REQ :twitch.tv/tags twitch.tv/commands");
send("PASS oauth:%s".formatted(twitchLogin.getAccessToken()));
sendMessage("NICK %s".formatted(twitchLogin.getUsername().toLowerCase()));
}
}

@Override
public void onMessage(String messageStr){
try(var logContext = LogContext.empty().withSocketId(uuid)){
log.trace("Received IRC Websocket message: {}", messageStr.strip());
}
catch(Exception e){
log.error("Failed to handle IRC WebSocket message {}", messageStr, e);
}
}

@Override
public void onClose(int code, String reason, boolean remote){
try(var ignored = LogContext.empty().withSocketId(uuid)){
log.info("IRC WebSocket closed with code {}, from host {}, reason {}", code, remote, reason);
listeners.forEach(l -> l.onWebSocketClosed(this, code, reason, remote));
}
}

@Override
public void onError(Exception e){
log.error("Error from IRC WebSocket", e);
}

private void onPing(){
lastPing = TimeFactory.now();
log.debug("Received IRC Ping request");
}

private void sendMessage(@NotNull String message){
log.trace("Sending IRC message {}", message);
send(message);
}

@Override
public void onWebsocketPing(WebSocket conn, Framedata f){
onPing();
sendMessage("PONG");
}

@Override
public void join(@NotNull String channel){
try(var ignored = LogContext.empty().withSocketId(uuid)){
if(channels.add(channel)){
sendMessage("JOIN #" + channel);
}
}
}

@Override
public void leave(@NotNull String channel){
try(var ignored = LogContext.empty().withSocketId(uuid)){
sendMessage("PART #" + channel);
channels.remove(channel);
}
}

public boolean isChannelJoined(@NotNull String channel){
return channels.contains(channel);
}

public void addListener(ITwitchChatWebSocketListener listener){
listeners.add(listener);
}

public long getChannelCount(){
return getChannels().size();
}
}
Loading

0 comments on commit 490afa9

Please sign in to comment.