Skip to content

Commit

Permalink
Add badge icons/emotes
Browse files Browse the repository at this point in the history
Badges in twitch messages now display as emotes.
They can be disabled as any emotes, this causes the old badge texts to be used.
Also, added support for displaying more than one badge at once, both image and text type
This fixes #6 (or rather implements that idea, but ya know - github wouldn't auto-close that issue if I wrote "implements")
  • Loading branch information
mini-bomba committed Jan 8, 2022
1 parent e7cb6cc commit 8fea193
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 9 deletions.
13 changes: 12 additions & 1 deletion src/main/java/me/mini_bomba/streamchatmod/StreamChatMod.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ public void postInit(FMLPostInitializationEvent event) {
if (config.updateCheckerEnabled.getBoolean()) startUpdateChecker();
progress.step("Syncing emote cache");
if (twitch != null) {
ProgressManager.ProgressBar emoteProgress = ProgressManager.push("Syncing emotes", 7);
ProgressManager.ProgressBar emoteProgress = ProgressManager.push("Syncing emotes", 9);
emotes.syncGlobalBadges(emoteProgress, false);
emotes.syncGlobalEmotes(emoteProgress, false);
emotes.syncAllChannelEmotes(emoteProgress, Arrays.stream(config.twitchChannels.getStringList()).map(this::getTwitchUserByName).map(User::getId).collect(Collectors.toList()), false);
ProgressManager.pop(emoteProgress);
Expand Down Expand Up @@ -236,6 +237,14 @@ protected List<Emote> queryGlobalTwitchEmotes() {
return twitch.getHelix().getGlobalEmotes(null).execute().getEmotes();
}

protected List<ChatBadgeSet> queryGlobalTwitchBadges() {
if (twitch == null) {
LOGGER.warn("Could not get global Twitch badges: Twitch client is disabled");
return Collections.emptyList();
}
return twitch.getHelix().getGlobalChatBadges(null).execute().getBadgeSets();
}

/**
* Schedules an action to be run in another thread.<br>
* <b>This will throw a ConcurrentModificationException if an important action is scheduled</b> (such as Twitch client stopping)<br>
Expand Down Expand Up @@ -550,6 +559,8 @@ public boolean startTwitch(boolean syncEmotes) {
.withEnableTMI(true)
.build();
if (syncEmotes) {
StreamUtils.queueAddMessage(EnumChatFormatting.GRAY + "Synchronising global badge cache...");
emotes.syncGlobalBadges(null, true);
StreamUtils.queueAddMessage(EnumChatFormatting.GRAY + "Synchronising global emote cache...");
emotes.syncGlobalEmotes(null, true);
StreamUtils.queueAddMessage(EnumChatFormatting.GRAY + "Synchronising channel emote cache...");
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/me/mini_bomba/streamchatmod/StreamConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class StreamConfig {
// emotes
public final Property showTwitchGlobalEmotes;
public final Property showTwitchChannelEmotes;
public final Property showTwitchGlobalBadges;
public final Property showBTTVGlobalEmotes;
public final Property showBTTVChannelEmotes;
public final Property showFFZGlobalEmotes;
Expand Down Expand Up @@ -81,6 +82,7 @@ public StreamConfig(File configFile) {
// emotes
showTwitchGlobalEmotes = config.get("emotes", "twitch_globals", true);
showTwitchChannelEmotes = config.get("emotes", "twitch_channel", true);
showTwitchGlobalBadges = config.get("emotes", "twitch_global_badges", true);
showBTTVGlobalEmotes = config.get("emotes", "bttv_globals", true);
showBTTVChannelEmotes = config.get("emotes", "bttv_channel", true);
showFFZGlobalEmotes = config.get("emotes", "ffz_globals", true);
Expand Down
87 changes: 86 additions & 1 deletion src/main/java/me/mini_bomba/streamchatmod/StreamEmotes.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package me.mini_bomba.streamchatmod;

import com.github.twitch4j.helix.domain.ChatBadge;
import com.github.twitch4j.helix.domain.ChatBadgeSet;
import com.github.twitch4j.helix.domain.Emote;
import me.mini_bomba.streamchatmod.utils.*;
import net.minecraft.client.Minecraft;
Expand Down Expand Up @@ -31,6 +33,7 @@ public class StreamEmotes {
private final Map<String, BTTVStreamEmote> bttvEmotes = new HashMap<>();
private final Map<String, FFZStreamEmote> ffzEmotes = new HashMap<>();
private final Map<String, Map<String, StreamEmote>> channelEmotes = new HashMap<>();
private final Map<String, TwitchGlobalBadge> globalBadges = new HashMap<>();

public StreamEmotes(StreamChatMod mod) {
this.mod = mod;
Expand Down Expand Up @@ -81,6 +84,77 @@ public StreamEmote getEmote(String channelId, String name) {
return null;
}

public TwitchGlobalBadge getGlobalBadge(String nameAndVersion) {
return globalBadges.getOrDefault(nameAndVersion, null);
}

public TwitchGlobalBadge getGlobalBadge(String name, String version) {
return globalBadges.getOrDefault(name + ":" + version, null);
}

public void syncGlobalBadges(ProgressManager.ProgressBar progress, boolean indexInMainThread) {
// Twitch
if (progress != null) progress.step("Twitch global badges");
File twitchBadgesDir = new File("streamchatmod/emotes/twitch_global_badges");
twitchBadgesDir.mkdirs();
List<String> cachedTwitchBadges = Arrays.stream(twitchBadgesDir.list())
.map(name -> name.endsWith("_3x.png") ? name.substring(0, name.length() - 7) : name)
.collect(Collectors.toList());
List<ChatBadgeSet> twitchBadgeSets = mod.queryGlobalTwitchBadges();
List<TwitchBadge> twitchBadges = twitchBadgeSets.stream().flatMap(set -> set.getVersions().stream().map(badge -> new TwitchBadge(set, badge))).collect(Collectors.toList());
List<TwitchBadge> twitchBadgesToDownload = twitchBadges.stream()
.filter(badge -> !cachedTwitchBadges.contains(badge.id))
.collect(Collectors.toList());
threadedDownload(progress != null, twitchBadgesToDownload.stream().map((Function<TwitchBadge, Function<ProgressManager.ProgressBar, Callable<Void>>>) badge -> downloadProgress -> () -> {
try {
FileUtils.copyURLToFile(
new URL(badge.badge.getLargeImageUrl()),
new File("streamchatmod/emotes/twitch_global_badges/" + badge.id + "_3x.png")
);
} catch (Exception e) {
LOGGER.warn("Failed to download Twitch global badge " + badge.set.getSetId() + ":" + badge.badge.getId());
e.printStackTrace();
}
if (downloadProgress != null) synchronized (downloadProgress) {
downloadProgress.step("Downloaded " + badge.set.getSetId() + ":" + badge.badge.getId());
}
return null;
}).collect(Collectors.toList()));

// Indexing
if (progress != null) progress.step("Indexing global badges");
Callable<Void> doIndex = () -> {
java.util.stream.Stream<StreamEmote> stream1 = twitchBadges.stream().map(badge -> {
if (twitchEmotes.containsKey(badge.id)) return twitchEmotes.get(badge.id);
try {
TwitchGlobalBadge wrappedBadge = new TwitchGlobalBadge(badge.badge, badge.set);
globalBadges.put(badge.id, wrappedBadge);
return wrappedBadge;
} catch (IOException e) {
LOGGER.warn("Failed to wrap global twitch badge " + badge.set.getSetId() + ":" + badge.badge.getId() + " in TwitchGlobalBadge class");
e.printStackTrace();
return null;
}
});
globalBadges.clear();
stream1.forEach(badge -> {
if (badge == null) return;
if (badge instanceof TwitchGlobalBadge && !namesToGlobalEmotes.containsKey(badge.name))
globalBadges.put(badge.name, (TwitchGlobalBadge) badge);
else LOGGER.warn("Duplicate badge name: " + badge.name);
});
return null;
};
try {
if (indexInMainThread) Minecraft.getMinecraft().addScheduledTask(doIndex).get();
else doIndex.call();
} catch (InterruptedException ignored) {
} catch (Exception e) {
LOGGER.error("Got error while indexing global badges");
e.printStackTrace();
}
}

public void syncGlobalEmotes(ProgressManager.ProgressBar progress, boolean indexInMainThread) {
// Twitch
if (progress != null) progress.step("Twitch global emotes");
Expand Down Expand Up @@ -227,7 +301,6 @@ public void syncGlobalEmotes(ProgressManager.ProgressBar progress, boolean index
}
}


public void syncAllChannelEmotes(ProgressManager.ProgressBar progress, List<String> channelIds, boolean indexInMainThread) {
// BTTV
if (progress != null) progress.step("BetterTTV channel emotes");
Expand Down Expand Up @@ -454,4 +527,16 @@ private static void threadedDownload(List<Callable<Void>> downloads) {
} catch (InterruptedException ignored) {
}
}

private static class TwitchBadge {
public final ChatBadgeSet set;
public final ChatBadge badge;
public final String id;

private TwitchBadge(ChatBadgeSet set, ChatBadge badge) {
this.set = set;
this.badge = badge;
this.id = TwitchGlobalBadge.getBadgeId(badge);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,32 @@ private List<IChatComponent> processEmotes(String message) {
public void run() {
boolean showChannel = mod.config.forceShowChannelName.getBoolean() || (mod.twitch != null && mod.twitch.getChat().getChannels().size() > 1);
Set<CommandPermission> perms = event.getPermissions();
String badges = perms.contains(CommandPermission.BROADCASTER) ? EnumChatFormatting.RED + "STREAMER " :
(perms.contains(CommandPermission.TWITCHSTAFF) ? EnumChatFormatting.BLACK + "STAFF " :
(perms.contains(CommandPermission.MODERATOR) ? EnumChatFormatting.GREEN + "MOD " :
(perms.contains(CommandPermission.VIP) ? EnumChatFormatting.LIGHT_PURPLE + "VIP " :
(perms.contains(CommandPermission.SUBSCRIBER) ? EnumChatFormatting.GOLD + "SUB " : ""))));
boolean allowFormatting = mod.config.allowFormatting.getBoolean() && (badges.length() > 1 || !mod.config.subOnlyFormatting.getBoolean());
IChatComponent badges = new ChatComponentText("");
if (mod.config.showTwitchGlobalBadges.getBoolean())
event.getMessageEvent().getBadges().forEach((name, version) -> badges.appendSibling(new ChatComponentStreamEmote(mod, mod.emotes.getGlobalBadge(name, version))));
else {
ArrayList<String> badgesTexts = new ArrayList<>();
if (perms.contains(CommandPermission.BROADCASTER))
badgesTexts.add(EnumChatFormatting.RED + "STREAMER");
if (perms.contains(CommandPermission.TWITCHSTAFF))
badgesTexts.add(EnumChatFormatting.BLACK + "STAFF");
if (perms.contains(CommandPermission.MODERATOR) && !perms.contains(CommandPermission.BROADCASTER))
badgesTexts.add(EnumChatFormatting.GREEN + "MOD");
if (perms.contains(CommandPermission.VIP))
badgesTexts.add(EnumChatFormatting.LIGHT_PURPLE + "VIP");
if (perms.contains(CommandPermission.SUBSCRIBER))
badgesTexts.add(EnumChatFormatting.GOLD + "SUB");
if (badgesTexts.size() > 0)
badges.appendSibling(new ChatComponentText(StringUtils.join(badgesTexts, " ")));
}
boolean allowFormatting = mod.config.allowFormatting.getBoolean() && (!mod.config.subOnlyFormatting.getBoolean() || perms.stream().anyMatch(p -> p == CommandPermission.SUBSCRIBER || p == CommandPermission.VIP || p == CommandPermission.MODERATOR || p == CommandPermission.TWITCHSTAFF || p == CommandPermission.BROADCASTER));
String message = event.getMessage();

Matcher matcher = urlPattern.matcher(message);
List<ClipComponentMapping> clips = new ArrayList<>();
IChatComponent component = new ChatComponentTwitchMessage(event.getMessageEvent().getMessageId().orElse(""), event.getChannel().getId(), event.getUser().getId(), StreamUtils.createPrefixedString(mod.config, badges + EnumChatFormatting.WHITE + event.getUser().getName() + " " + mod.config.getTwitchUserMessageSeparator() + " ", showChannel ? event.getChannel().getName() : null));
IChatComponent component = new ChatComponentTwitchMessage(event.getMessageEvent().getMessageId().orElse(""), event.getChannel().getId(), event.getUser().getId(), (showChannel ? mod.config.getTwitchPrefixWithChannel(event.getChannel().getName()) : mod.config.getFullTwitchPrefix()) + " ");
if (badges.getSiblings().size() > 0) component.appendSibling(badges);
component.appendSibling(new ChatComponentText((badges.getSiblings().size() > 0 ? " " : "") + EnumChatFormatting.WHITE + event.getUser().getName() + " " + mod.config.getTwitchUserMessageSeparator() + " "));
int lastEnd = 0;
while (matcher.find()) {
if (matcher.start() > lastEnd)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public String getEmoteLink() {
return ((BTTVStreamEmote) emote).emote.getLargeEmoteURL();
if (emote instanceof FFZStreamEmote)
return ((FFZStreamEmote) emote).emote.getLargeEmoteURL();
if (emote instanceof TwitchGlobalBadge)
return ((TwitchGlobalBadge) emote).badge.getLargeImageUrl();
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ public char getCharacter() {
public enum Type {
TWITCH_GLOBAL("Twitch global emote"),
TWITCH_CHANNEL("Twitch channel emote"),
TWITCH_GLOBAL_BADGE("Twitch global badge"),
BTTV_GLOBAL("BetterTTV global emote"),
BTTV_CHANNEL("BetterTTV channel emote"),
FFZ_GLOBAL("FrankerFaceZ global emote"),
Expand All @@ -152,6 +153,8 @@ public static Property getConfigProperty(Type type, StreamConfig config) {
return config.showTwitchGlobalEmotes;
case TWITCH_CHANNEL:
return config.showTwitchChannelEmotes;
case TWITCH_GLOBAL_BADGE:
return config.showTwitchGlobalBadges;
case BTTV_GLOBAL:
return config.showBTTVGlobalEmotes;
case BTTV_CHANNEL:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package me.mini_bomba.streamchatmod.utils;

import com.github.twitch4j.helix.domain.ChatBadge;
import com.github.twitch4j.helix.domain.ChatBadgeSet;

import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class TwitchGlobalBadge extends StreamEmote {
private static final Pattern idPattern = Pattern.compile("https://static-cdn.jtvnw.net/badges/v1/([\\da-f-]+)/\\d");
public final String id;
public final ChatBadgeSet set;
public final ChatBadge badge;

public TwitchGlobalBadge(ChatBadge badge, ChatBadgeSet set) throws IOException {
super(Type.TWITCH_GLOBAL_BADGE, getBadgeId(badge), "streamchatmod/emotes/twitch_global_badges/" + getBadgeId(badge) + "_3x.png", set.getSetId() + ":" + badge.getId(), false);
this.badge = badge;
this.set = set;
this.id = getBadgeId(badge);
}

public static String getBadgeId(ChatBadge badge) {
Matcher m = idPattern.matcher(badge.getSmallImageUrl());
return m.find() ? m.group(1) : null;
}
}

0 comments on commit 8fea193

Please sign in to comment.