Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve chat regex matching #616

Merged
merged 13 commits into from Jul 31, 2022
Expand Up @@ -25,8 +25,8 @@
@EqualsAndHashCode(callSuper = false)
public class IRCMessageEvent extends TwitchEvent {

private static final Pattern MESSAGE_PATTERN = Pattern.compile("^(?:@(?<tags>.+?)\\s)?(?<clientName>.+?)\\s(?<command>[A-Z0-9]+)\\s?(?:#(?<channel>.*?)\\s?)?(?<payload>[:\\-+](?<message>.+))?$");
private static final Pattern WHISPER_PATTERN = Pattern.compile("^(?:@(?<tags>.+?)\\s)?:(?<clientName>.+?)!.+?\\s(?<command>[A-Z0-9]+)\\s(?:(?<channel>.*?)\\s?)??(?<payload>[:\\-+](?<message>.+))$");
private static final Pattern MESSAGE_PATTERN = Pattern.compile("^(?:@(?<tags>\\S+?)\\s)?(?<clientName>\\S+?)\\s(?<command>[A-Z0-9]+)\\s?(?:#(?<channel>\\S*?)\\s?)?(?<payload>[:\\-+](?<message>.+))?$");
private static final Pattern WHISPER_PATTERN = Pattern.compile("^(?:@(?<tags>\\S+?)\\s)?:(?<clientName>\\S+?)!.+?\\s(?<command>[A-Z0-9]+)\\s(?:(?<channel>\\S*?)\\s?)??(?<payload>[:\\-+](?<message>.+))$");
private static final Pattern CLIENT_PATTERN = Pattern.compile("^:(.*?)!(.*?)@(.*?).tmi.twitch.tv$");

@Unofficial
Expand Down Expand Up @@ -232,7 +232,9 @@ public String getUserName() {
return tags.get("login");
}

return getClientName().orElse(null);
return getClientName()
.filter(StringUtils::isNotBlank)
.orElseGet(() -> getTagValue("display-name").orElse(null));
}

/**
Expand Down
@@ -0,0 +1,149 @@
package com.github.twitch4j.chat.events.channel;

import com.github.twitch4j.common.enums.CommandPermission;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.util.Collections;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

@Tag("unittest")
public class IRCMessageEventTest {

@Test
@DisplayName("Tests that CLEARCHAT is parsed by IRCMessageEvent")
void parseChatClear() {
IRCMessageEvent e = build("@room-id=12345678;tmi-sent-ts=1642715756806 :tmi.twitch.tv CLEARCHAT #dallas");

assertEquals("CLEARCHAT", e.getCommandType());
assertEquals("dallas", e.getChannelName().orElse(null));
assertEquals("12345678", e.getChannelId());
}

@Test
@DisplayName("Tests that CLEARMSG is parsed by IRCMessageEvent")
void parseMessageDeletion() {
IRCMessageEvent e = build("@login=foo;room-id=;target-msg-id=94e6c7ff-bf98-4faa-af5d-7ad633a158a9;tmi-sent-ts=1642720582342 :tmi.twitch.tv CLEARMSG #bar :what a great day");

assertEquals("foo", e.getUserName());
assertEquals("bar", e.getChannelName().orElse(null));
assertEquals("CLEARMSG", e.getCommandType());
assertEquals("what a great day", e.getMessage().orElse(null));
assertEquals("94e6c7ff-bf98-4faa-af5d-7ad633a158a9", e.getTagValue("target-msg-id").orElse(null));
}

@Test
@DisplayName("Tests that GLOBALUSERSTATE is parsed by IRCMessageEvent")
void parseGlobalUserState() {
IRCMessageEvent e = build("@badge-info=subscriber/8;badges=subscriber/6;color=#0D4200;display-name=dallas;emote-sets=0,33,50,237,793,2126,3517,4578,5569,9400,10337,12239;turbo=0;user-id=12345678;user-type=admin " +
":tmi.twitch.tv GLOBALUSERSTATE");

assertEquals("GLOBALUSERSTATE", e.getCommandType());
assertEquals("12345678", e.getUserId());
assertEquals("dallas", e.getTagValue("display-name").orElse(null));
assertEquals("dallas", e.getUserName());
assertEquals("0,33,50,237,793,2126,3517,4578,5569,9400,10337,12239", e.getTagValue("emote-sets").orElse(null));
}

@Test
@DisplayName("Test that normal messages are parsed by IRCMessageEvent")
void parseMessage() {
IRCMessageEvent e = build("@badge-info=;badges=broadcaster/1;client-nonce=459e3142897c7a22b7d275178f2259e0;color=#0000FF;display-name=lovingt3s;emote-only=1;emotes=62835:0-10;first-msg=0;flags=;" +
"id=885196de-cb67-427a-baa8-82f9b0fcd05f;mod=0;room-id=713936733;subscriber=0;tmi-sent-ts=1643904084794;turbo=0;user-id=713936733;user-type= " +
":lovingt3s!lovingt3s@lovingt3s.tmi.twitch.tv PRIVMSG #lovingt3s :bleedPurple");

assertEquals("bleedPurple", e.getMessage().orElse(null));
assertEquals("lovingt3s", e.getChannelName().orElse(null));
assertEquals("713936733", e.getChannelId());
assertEquals("713936733", e.getUserId());
assertEquals("lovingt3s", e.getUserName());
assertEquals("PRIVMSG", e.getCommandType());
assertTrue(e.getClientPermissions().contains(CommandPermission.BROADCASTER));
assertEquals("885196de-cb67-427a-baa8-82f9b0fcd05f", e.getMessageId().orElse(null));
assertEquals("459e3142897c7a22b7d275178f2259e0", e.getNonce().orElse(null));
assertEquals("62835:0-10", e.getTagValue("emotes").orElse(null));
}

@Test
@DisplayName("Tests that NOTICE is parsed by IRCMessageEvent")
void parseNotice() {
IRCMessageEvent e = build("@msg-id=delete_message_success :tmi.twitch.tv NOTICE #bar :The message from foo is now deleted.");

assertEquals("NOTICE", e.getCommandType());
assertEquals("bar", e.getChannelName().orElse(null));
assertEquals("delete_message_success", e.getTags().get("msg-id"));
assertEquals("The message from foo is now deleted.", e.getMessage().orElse(null));
}

@Test
@DisplayName("Tests that RECONNECT is parsed by IRCMessageEvent")
void parseReconnect() {
IRCMessageEvent e = build(":tmi.twitch.tv RECONNECT");
assertEquals("RECONNECT", e.getCommandType());
}

@Test
@DisplayName("Tests that ROOMSTATE is parsed by IRCMessageEvent")
void parseRoomState() {
IRCMessageEvent e = build("@emote-only=0;followers-only=-1;r9k=0;rituals=0;room-id=12345678;slow=0;subs-only=0 :tmi.twitch.tv ROOMSTATE #bar");
assertEquals("ROOMSTATE", e.getCommandType());
assertEquals("bar", e.getChannelName().orElse(null));
assertEquals("12345678", e.getChannelId());
assertEquals("0", e.getTags().get("emote-only"));
assertEquals("-1", e.getTags().get("followers-only"));
}

@Test
@DisplayName("Tests that USERNOTICE is parsed by IRCMessageEvent")
void parseUserNotice() {
IRCMessageEvent e = build("@badge-info=;badges=staff/1,premium/1;color=#0000FF;display-name=TWW2;emotes=;id=e9176cd8-5e22-4684-ad40-ce53c2561c5e;login=tww2;mod=0;msg-id=subgift;" +
"msg-param-months=1;msg-param-recipient-display-name=Mr_Woodchuck;msg-param-recipient-id=55554444;msg-param-recipient-name=mr_woodchuck;msg-param-sub-plan-name=House\\sof\\sNyoro~n;msg-param-sub-plan=1000;" +
"room-id=12345678;subscriber=0;system-msg=TWW2\\sgifted\\sa\\sTier\\s1\\ssub\\sto\\sMr_Woodchuck!;tmi-sent-ts=1521159445153;turbo=0;user-id=87654321;user-type=staff :tmi.twitch.tv USERNOTICE #forstycup");

assertEquals("USERNOTICE", e.getCommandType());
assertEquals("12345678", e.getChannelId());
assertEquals("forstycup", e.getChannelName().orElse(null));
assertEquals("87654321", e.getUserId());
assertEquals("TWW2 gifted a Tier 1 sub to Mr_Woodchuck!", e.getTagValue("system-msg").orElse(null));
assertEquals("TWW2", e.getTagValue("display-name").orElse(null));
assertEquals("tww2", e.getUserName());
assertEquals("subgift", e.getTagValue("msg-id").orElse(null));
}

@Test
@DisplayName("Tests that USERSTATE is parsed by IRCMessageEevent")
void parseUserState() {
IRCMessageEvent e = build("@badge-info=;badges=staff/1;color=#0D4200;display-name=ronni;emote-sets=0,33,50,237,793,2126,3517,4578,5569,9400,10337,12239;mod=1;subscriber=1;turbo=1;user-type=staff " +
":tmi.twitch.tv USERSTATE #dallas");

assertEquals("USERSTATE", e.getCommandType());
assertEquals("dallas", e.getChannelName().orElse(null));
assertEquals("ronni", e.getUserName());
assertEquals("0,33,50,237,793,2126,3517,4578,5569,9400,10337,12239", e.getTagValue("emote-sets").orElse(null));
assertTrue(e.getClientPermissions().contains(CommandPermission.TWITCHSTAFF));
}

@Test
@DisplayName("Test that whispers are parsed by IRCMessageEvent")
void parseWhisper() {
IRCMessageEvent e = build("@badges=;color=;display-name=HexaFice;emotes=;message-id=103;thread-id=142621956_149223493;turbo=0;user-id=142621956;user-type= " +
":hexafice!hexafice@hexafice.tmi.twitch.tv WHISPER twitch4j :test 123");

assertEquals("test 123", e.getMessage().orElse(null));
assertEquals("WHISPER", e.getCommandType());
assertEquals("142621956", e.getUserId());
assertEquals("hexafice", e.getUserName());
assertEquals("HexaFice", e.getTagValue("display-name").orElse(null));
assertEquals("twitch4j", e.getChannelName().orElse(null));
assertTrue(e.getBadges() == null || e.getBadges().isEmpty());
assertTrue(e.getBadgeInfo() == null || e.getBadgeInfo().isEmpty());
}

private static IRCMessageEvent build(String raw) {
return new IRCMessageEvent(raw, Collections.emptyMap(), Collections.emptyMap(), Collections.emptySet());
}

}
Expand Up @@ -152,10 +152,10 @@ public static Map<String, String> parseBadges(String raw) {
if (StringUtils.isBlank(raw)) return map;

// Fix Whitespaces
raw = raw.replace("\\s", " ");
raw = EscapeUtils.unescapeTagValue(raw);

for (String tag : raw.split(",")) {
String[] val = tag.split("/");
for (String tag : StringUtils.split(raw, ',')) {
String[] val = StringUtils.split(tag, "/", 2);
final String key = val[0];
String value = (val.length > 1) ? val[1] : null;
map.put(key, value);
Expand Down
@@ -0,0 +1,41 @@
package com.github.twitch4j.common.util;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.util.HashMap;
import java.util.Map;

import static com.github.twitch4j.common.util.TwitchUtils.parseBadges;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static org.junit.jupiter.api.Assertions.assertEquals;

@Tag("unittest")
class TwitchUtilsTest {

@Test
@DisplayName("Tests TwitchUtils.parseBadges")
void badgesParseTest() {
assertEquals(emptyMap(), parseBadges(null));
assertEquals(emptyMap(), parseBadges(""));
assertEquals(emptyMap(), parseBadges(" "));

assertEquals(singletonMap("subscriber", "15"), parseBadges("subscriber/15"));
assertEquals(singletonMap("subscriber", "15/3"), parseBadges("subscriber/15/3"));
assertEquals(singletonMap("a b", "c d"), parseBadges("a\\sb/c\\sd"));

assertEquals(mapOf("subscriber", "18", "no_audio", "1"), parseBadges("subscriber/18,no_audio/1"));
assertEquals(mapOf("subscriber", "19", "no_audio", null), parseBadges("subscriber/19,no_audio/"));
assertEquals(mapOf("follower", "20", "no_video", null), parseBadges("follower/20,no_video"));
}

private static <K, V> Map<K, V> mapOf(K key1, V value1, K key2, V value2) {
Map<K, V> map = new HashMap<>();
map.put(key1, value1);
map.put(key2, value2);
return map;
}

}