Skip to content

Commit

Permalink
fix: improve chat regex matching (#616)
Browse files Browse the repository at this point in the history
* fix: improve chat regex matching

* chore: add unit tests for parseBadges

* chore: add unit test for whisper parsing

* chore: add unit test for privmsg parsing

* chore: add unit test for clearmsg parsing

* chore: add unit test for clearchat parsing

* chore: add unit test for globaluserstate parsing

* fix: use display name when login and client are missing

* chore: add unit test for notice parsing

* chore: add unit test for reconnect parsing

* chore: add unit test for roomstate parsing

* chore: add unit test for usernotice parsing

* chore: add unit test for userstate parsing
  • Loading branch information
iProdigy committed Jul 31, 2022
1 parent aa236ff commit 92b0511
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 6 deletions.
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;
}

}

0 comments on commit 92b0511

Please sign in to comment.