Skip to content

Commit

Permalink
feat(tsl): add Twitch Chat Message event
Browse files Browse the repository at this point in the history
- Added new TSL Event: Twitch Chat Message
- Fixed the multi-trigger on WebSocketTracer for good
  • Loading branch information
iGoodie committed Jun 18, 2020
1 parent 706258f commit 97ca396
Show file tree
Hide file tree
Showing 14 changed files with 318 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .idea/runConfigurations/runClient.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Expand Up @@ -155,7 +155,8 @@ private static boolean correctStreamers(Config config, String path, ObjectConver
if (streamer.twitchNick == null
|| streamer.minecraftNick == null
|| streamer.platform == null
|| streamer.token == null) {
|| streamer.token == null
|| streamer.tokenChat == null) {
TwitchSpawn.LOGGER.info("Correcting {}: Streamer on index {} is missing some fields -> {}", path, i, element);
streamer = Streamer.from(streamer, new Streamer());
streamers.set(i, toConfig(converter, streamer));
Expand Down Expand Up @@ -197,13 +198,15 @@ public static Streamer from(Streamer other, Streamer defaultStreamer) {
created.twitchNick = (other.twitchNick != null ? other : defaultStreamer).twitchNick;
created.platform = (other.platform != null ? other : defaultStreamer).platform;
created.token = (other.token != null ? other : defaultStreamer).token;
created.tokenChat = (other.tokenChat != null ? other : defaultStreamer).tokenChat;
return created;
}

public String minecraftNick = "MC_NICK";
public String twitchNick = "TWITCH_NICK";
public Platform platform = Platform.STREAMLABS;
public String token = "YOUR_TOKEN_HERE";
public String tokenChat = "YOUR_CHAT_TOKEN_HERE";

public Streamer() {}

Expand All @@ -218,7 +221,9 @@ public String toString() {
.append("minecraftNick=").append(minecraftNick).append(",")
.append("twitchNick=").append(twitchNick).append(",")
.append("platform=").append(platform).append(",")
.append("token=").append(token != null ? token.replaceAll("\\w", "#") : null).append("}")
.append("token=").append(token != null ? token.replaceAll("\\w", "#") : null)
.append("tokenChat=").append(tokenChat != null ? tokenChat.replaceAll("\\w", "#") : null)
.append("}")
.toString();
}
}
Expand Down
@@ -0,0 +1,12 @@
package net.programmer.igoodie.twitchspawn.easteregg;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class Developers {

public static final Set<String> TWITCH_NICKS = new HashSet<>(
Arrays.asList("iGoodiex", "TheDiaval", "Girloflegend"));

}
Expand Up @@ -11,6 +11,10 @@ public enum Platform {
TWITCH_PUBSUB(
"Twitch PubSub",
"wss://pubsub-edge.twitch.tv"
),
TWITCH_CHAT_IRC(
"Twitch Chat IRC",
"wss://irc-ws.chat.twitch.tv:443"
);

public static Platform withName(String name) {
Expand Down
Expand Up @@ -9,6 +9,10 @@
import net.programmer.igoodie.twitchspawn.configuration.CredentialsConfig;
import net.programmer.igoodie.twitchspawn.network.NetworkManager;
import net.programmer.igoodie.twitchspawn.network.packet.StatusChangedPacket;
import net.programmer.igoodie.twitchspawn.tracer.chat.TwitchChatTracer;
import net.programmer.igoodie.twitchspawn.tracer.socket.StreamElementsSocketTracer;
import net.programmer.igoodie.twitchspawn.tracer.socket.StreamlabsSocketTracer;
import net.programmer.igoodie.twitchspawn.tracer.socket.TwitchPubSubTracer;

import java.util.HashMap;
import java.util.LinkedList;
Expand All @@ -23,9 +27,7 @@ public class TraceManager {

public TraceManager() {
this.sockets = new HashMap<>();

this.webSocketTracers = new LinkedList<>();
this.webSocketTracers.add(new TwitchPubSubTracer(this));
}

public boolean isRunning() {
Expand All @@ -42,6 +44,8 @@ public void start() {
running = true;

// Start Websocket tracers
this.webSocketTracers.add(new TwitchPubSubTracer(this)); // TODO: Extract to a worker, not master
this.webSocketTracers.add(new TwitchChatTracer(this)); // TODO: Extract to a worker, not master
webSocketTracers.forEach(WebSocketTracer::start);

// Connect online players from credentials.toml
Expand Down
Expand Up @@ -2,6 +2,7 @@

import net.programmer.igoodie.twitchspawn.configuration.CredentialsConfig;
import okhttp3.*;
import okio.ByteString;

import java.util.LinkedList;
import java.util.List;
Expand All @@ -11,12 +12,12 @@ public abstract class WebSocketTracer {

protected TraceManager manager;
protected Platform api;
protected List<OkHttpClient> clients;
protected List<WebSocket> sockets;

public WebSocketTracer(Platform api, TraceManager manager) {
this.manager = manager;
this.api = api;
this.clients = new LinkedList<>();
this.sockets = new LinkedList<>();
}

public abstract void start();
Expand All @@ -35,6 +36,11 @@ public void onMessage(WebSocket webSocket, String text) {
WebSocketTracer.this.onMessage(streamer, webSocket, text);
}

@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
WebSocketTracer.this.onMessage(streamer, webSocket, bytes.toString());
}

@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
WebSocketTracer.this.onClosing(streamer, webSocket, code, reason);
Expand All @@ -47,7 +53,7 @@ public void onFailure(WebSocket webSocket, Throwable t, Response response) {
};
}

protected OkHttpClient startClient(WebSocketListener socket) {
protected WebSocket startClient(WebSocketListener listener) {
OkHttpClient client = new OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS)
.build();
Expand All @@ -56,9 +62,7 @@ protected OkHttpClient startClient(WebSocketListener socket) {
.url(api.url)
.build();

client.newWebSocket(connectRequest, socket);

return client;
return client.newWebSocket(connectRequest, listener);
}

protected void onOpen(CredentialsConfig.Streamer streamer, WebSocket socket, Response response) {}
Expand Down
@@ -0,0 +1,117 @@
package net.programmer.igoodie.twitchspawn.tracer.chat;

import net.programmer.igoodie.twitchspawn.TwitchSpawn;
import net.programmer.igoodie.twitchspawn.configuration.ConfigManager;
import net.programmer.igoodie.twitchspawn.configuration.CredentialsConfig;
import net.programmer.igoodie.twitchspawn.tracer.Platform;
import net.programmer.igoodie.twitchspawn.tracer.TraceManager;
import net.programmer.igoodie.twitchspawn.tracer.WebSocketTracer;
import net.programmer.igoodie.twitchspawn.tracer.model.TwitchChatMessage;
import net.programmer.igoodie.twitchspawn.tslanguage.EventArguments;
import net.programmer.igoodie.twitchspawn.util.CooldownBucket;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;

import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;

public class TwitchChatTracer extends WebSocketTracer {

// Streamer Nickname -> CooldownBucket
public Map<String, CooldownBucket> cooldownBuckets;

public TwitchChatTracer(TraceManager manager) {
super(Platform.TWITCH_CHAT_IRC, manager);
this.cooldownBuckets = new HashMap<>();
}

@Override
public void start() {
for (CredentialsConfig.Streamer streamer : ConfigManager.CREDENTIALS.streamers) {
WebSocketListener socket = createSocket(streamer);
this.sockets.add(startClient(socket));
this.cooldownBuckets.put(streamer.twitchNick, new CooldownBucket());
}
}

@Override
public void stop() {
for (WebSocket socket : this.sockets) {
if (!socket.close(1000, null)) {
socket.cancel();
}
}
this.cooldownBuckets.clear();
}

/* --------------------------------------------------- */

@Override
protected void onOpen(CredentialsConfig.Streamer streamer, WebSocket socket, Response response) {
TwitchSpawn.LOGGER.info("Attempting to connect Twitch Chat of {}", streamer.twitchNick);

socket.send("PASS " + streamer.tokenChat);
socket.send("NICK " + streamer.twitchNick.toLowerCase());
socket.send("JOIN #" + streamer.twitchNick.toLowerCase());
socket.send("CAP REQ :twitch.tv/tags");
socket.send("PRIVMSG #" + streamer.twitchNick.toLowerCase()
+ " :TwitchSpawn now connected to the chat! Hey folks!");

// https://twitchapps.com/tmi/
}

@Override
protected void onClosing(CredentialsConfig.Streamer streamer, WebSocket socket, int code, String reason) {
TwitchSpawn.LOGGER.info("Disconnected from {}'s Twitch Chat connection. (intentional)", streamer.minecraftNick);
}

@Override
protected void onMessage(CredentialsConfig.Streamer streamer, WebSocket socket, String text) {
Stream.of(text.split("\r?\n")).map(String::trim).forEach(message -> {
if (message.equals("PING :tmi.twitch.tv")) {
socket.send("PONG :tmi.twitch.tv");

} else if (TwitchChatMessage.matches(message)) {
TwitchChatMessage twitchChatMessage = new TwitchChatMessage(message);
onChatMessage(streamer, twitchChatMessage);

} else if (message.contains(":tmi.twitch.tv NOTICE")) {
if (message.contains("Improperly formatted auth")) {
// Intentionally left empty/malformed.
TwitchSpawn.LOGGER.info("Disconnected from {}'s Twitch Chat connection. (no token)", streamer.minecraftNick);
socket.cancel();

} else if (message.contains("Login authentication failed")) {
// Uh oh invalid token?
TwitchSpawn.LOGGER.warn("Disconnected from {}'s Twitch Chat connection. (unauthorized)", streamer.minecraftNick);
manager.stop(null, streamer.twitchNick + " unauthorized by the Twitch Chat server.");
}
}
});
}

protected void onChatMessage(CredentialsConfig.Streamer streamer, TwitchChatMessage twitchChatMessage) {
CooldownBucket cooldownBucket = cooldownBuckets.get(streamer.twitchNick);

if (cooldownBucket.canConsume(twitchChatMessage.username)) {
EventArguments eventArguments = new EventArguments("chat", "twitch");
eventArguments.streamerNickname = streamer.minecraftNick;
eventArguments.actorNickname = twitchChatMessage.username;
eventArguments.message = twitchChatMessage.message;
eventArguments.subscriptionMonths = twitchChatMessage.subscriptionMonths;
// TODO: Add badges too

ConfigManager.RULESET_COLLECTION.handleEvent(eventArguments, cooldownBucket);

} else {
TwitchSpawn.LOGGER.info("Still has {} seconds global cooldown.", cooldownBucket.getGlobalCooldown());
}
}

public static void main(String[] args) {
new TwitchChatTracer(new TraceManager()).start();
}

}
@@ -0,0 +1,72 @@
package net.programmer.igoodie.twitchspawn.tracer.model;

import net.programmer.igoodie.twitchspawn.easteregg.Developers;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

public class TwitchChatMessage {

public static final Pattern TWITCH_CHAT_PATTERN = Pattern.compile("^@(?<tags>.*?) (:(?<user>.*?)!.*?\\.tmi\\.twitch\\.tv) PRIVMSG #(?<channel>.*?) :(?<msg>.*)$");

private String raw;

public String username;
public String message;
public Set<String> badges; // admin, bits, broadcaster, global_mod, moderator, subscriber, staff, turbo, vip, glhf-pledge
public int subscriptionMonths;
public boolean isDeveloper;

public TwitchChatMessage(String raw) {
this.raw = raw;
this.badges = new HashSet<>();

Matcher matcher = TWITCH_CHAT_PATTERN.matcher(raw);

if (matcher.matches()) {
Map<String, String> tags = parseTags(matcher.group("tags"));

String displayName = tags.getOrDefault("display-name", "");
this.username = displayName.isEmpty() ? matcher.group("user") : displayName;

Stream.of(tags.getOrDefault("badges", "").split(",")).forEach(badgeRaw -> {
if (badgeRaw.isEmpty()) return;
String[] parts = badgeRaw.split("/", 2);
String badgeName = parts[0];
String badgeVersion = parts[1];
badges.add(badgeName);
});

Stream.of(tags.getOrDefault("badge-info", "").split(",")).forEach(infoRaw -> {
if (infoRaw.isEmpty()) return;
String[] parts = infoRaw.split("/", 2);
String infoName = parts[0];
String infoValue = parts[1];
if (infoName.equals("subscriber"))
subscriptionMonths = Integer.parseInt(infoValue);
});

this.isDeveloper = Developers.TWITCH_NICKS.contains(this.username);

this.message = matcher.group("msg");
}
}

public static Map<String, String> parseTags(String tagsRaw) {
Map<String, String> tags = new HashMap<>();

for (String tagPairRaw : tagsRaw.split(";")) {
String[] tagPair = tagPairRaw.split("=", 2);
tags.put(tagPair[0], tagPair[1]);
}

return tags;
}

public static boolean matches(String raw) {
return TWITCH_CHAT_PATTERN.matcher(raw).matches();
}

}
@@ -1,9 +1,12 @@
package net.programmer.igoodie.twitchspawn.tracer;
package net.programmer.igoodie.twitchspawn.tracer.socket;

import io.socket.client.Socket;
import net.programmer.igoodie.twitchspawn.TwitchSpawn;
import net.programmer.igoodie.twitchspawn.configuration.ConfigManager;
import net.programmer.igoodie.twitchspawn.configuration.CredentialsConfig;
import net.programmer.igoodie.twitchspawn.tracer.Platform;
import net.programmer.igoodie.twitchspawn.tracer.SocketIOTracer;
import net.programmer.igoodie.twitchspawn.tracer.TraceManager;
import net.programmer.igoodie.twitchspawn.tslanguage.EventArguments;
import net.programmer.igoodie.twitchspawn.tslanguage.event.TSLEventPair;
import net.programmer.igoodie.twitchspawn.tslanguage.keyword.TSLEventKeyword;
Expand Down
@@ -1,10 +1,13 @@
package net.programmer.igoodie.twitchspawn.tracer;
package net.programmer.igoodie.twitchspawn.tracer.socket;

import io.socket.client.IO;
import io.socket.client.Socket;
import net.programmer.igoodie.twitchspawn.TwitchSpawn;
import net.programmer.igoodie.twitchspawn.configuration.ConfigManager;
import net.programmer.igoodie.twitchspawn.configuration.CredentialsConfig;
import net.programmer.igoodie.twitchspawn.tracer.Platform;
import net.programmer.igoodie.twitchspawn.tracer.SocketIOTracer;
import net.programmer.igoodie.twitchspawn.tracer.TraceManager;
import net.programmer.igoodie.twitchspawn.tslanguage.EventArguments;
import net.programmer.igoodie.twitchspawn.tslanguage.event.TSLEventPair;
import net.programmer.igoodie.twitchspawn.tslanguage.keyword.TSLEventKeyword;
Expand Down

0 comments on commit 97ca396

Please sign in to comment.