package chatty.util.ffz;
import chatty.Helper;
import chatty.util.api.usericons.Usericon;
import chatty.util.StringUtil;
import chatty.util.UrlRequest;
import chatty.util.api.Emoticon;
import chatty.util.api.EmoticonUpdate;
import chatty.util.settings.Settings;
import java.util.*;
import java.util.logging.Logger;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
/**
* Request FrankerFaceZ emoticons,mod icons and bot names.
*
* @author tduva
*/
public class FrankerFaceZ {
private static final Logger LOGGER = Logger.getLogger(FrankerFaceZ.class.getName());
private enum Type { GLOBAL, ROOM, FEATURE_FRIDAY };
private final FrankerFaceZListener listener;
// State
private boolean botNamesRequested;
/**
* The channels that have already been requested in this session.
*/
private final Set<String> alreadyRequested
= Collections.synchronizedSet(new HashSet<String>());
/**
* The channels whose request is currently pending. Channels get removed
* from here again once the request result is received.
*/
private final Set<String> requestPending
= Collections.synchronizedSet(new HashSet<String>());
/**
* Feature Friday
*/
private static final long FEATURE_FRIDAY_UPDATE_DELAY = 6*60*60*1000;
private boolean featureFridayTimerStarted;
private String featureFridayChannel;
private int featureFridaySet = -1;
private final WebsocketManager ws;
public FrankerFaceZ(FrankerFaceZListener listener, Settings settings) {
this.listener = listener;
this.ws = new WebsocketManager(listener, settings);
}
public void connectWs() {
ws.connect();
}
public void disconnectWs() {
ws.disconnect();
}
public String getWsStatus() {
return ws.getStatus();
}
public void joined(String room) {
room = Helper.toStream(room);
ws.addRoom(room);
}
public void left(String room) {
room = Helper.toStream(room);
ws.removeRoom(room);
}
public void setFollowing(String user, String room, String following) {
ws.setFollowing(user, room, following);
}
/**
* Requests the emotes for the given channel and global emotes. It only
* requests each set of emotes once, unless {@code forcedUpdate} is true.
*
* @param stream The name of the channel/stream
* @param forcedUpdate Whether to update even if it was already requested
*/
public synchronized void requestEmotes(String stream, boolean forcedUpdate) {
stream = Helper.toStream(stream);
if (stream == null || stream.isEmpty()) {
return;
}
request(Type.ROOM, stream, forcedUpdate);
requestGlobalEmotes(false);
if (!botNamesRequested) {
requestBotNames();
botNamesRequested = true;
}
}
/**
* Request global FFZ emotes.
*
* @param forcedUpdate If this is true, it forces the update, otherwise it
* only requests the emotes when not already requested this session
*/
public synchronized void requestGlobalEmotes(boolean forcedUpdate) {
request(Type.GLOBAL, null, forcedUpdate);
requestFeatureFridayEmotes(forcedUpdate);
}
/**
* Start timer to check for Feature Friday emotes. Does nothing if the timer
* is already started.
*/
public synchronized void autoUpdateFeatureFridayEmotes() {
if (featureFridayTimerStarted) {
return;
}
featureFridayTimerStarted = true;
Timer timer = new Timer("FFZ Feature Friday", true);
timer.schedule(new TimerTask() {
@Override
public void run() {
requestFeatureFridayEmotes(true);
}
}, FEATURE_FRIDAY_UPDATE_DELAY, FEATURE_FRIDAY_UPDATE_DELAY);
}
/**
* Request Feature Friday emotes.
*
* @param forcedUpdate If this is true, it forces the update, otherwise it
* only requests the emotes when not already requested this session
*/
public synchronized void requestFeatureFridayEmotes(boolean forcedUpdate) {
request(Type.FEATURE_FRIDAY, null, forcedUpdate);
}
/**
* Issue a request of the given type and stream.
*
* <p>
* The URL which is used for the request is build from the paramters. Only
* requests each URL once, unless {@code forcedUpdate} is true. Always
* prevents the same URL from being requested twice at the same time.</p>
*
* <p>This is not safe to be called unsynchronized, because of the way
* check the URL for being already requested/pending is done.</p>
*
* @param type The type of request
* @param stream The stream, can be {@code null} if not needed for this type
* @param forcedUpdate Whether to request even if already requested before
*/
private void request(final Type type, final String stream, boolean forcedUpdate) {
final String url = getUrl(type, stream);
if (requestPending.contains(url)
|| (alreadyRequested.contains(url) && !forcedUpdate)) {
return;
}
alreadyRequested.add(url);
requestPending.add(url);
// Create request and run it in a seperate thread
UrlRequest request = new UrlRequest() {
@Override
public void requestResult(String result, int responseCode) {
requestPending.remove(url);
parseResult(type, stream, result);
}
};
request.setLabel("[FFZ]");
request.setUrl(url);
new Thread(request).start();
}
/**
* Gets the URL for the given request type and stream.
*
* @param type The type
* @param stream The stream, if applicable to the type
* @return The URL as a String
*/
private String getUrl(Type type, String stream) {
if (type == Type.GLOBAL) {
return "http://api.frankerfacez.com/v1/set/global";
} else if (type == Type.FEATURE_FRIDAY) {
if (stream == null) {
return "http://cdn.frankerfacez.com/script/event.json";
// return "http://127.0.0.1/twitch/ffz_feature";
} else {
// The stream is a set id in this case
return "https://api.frankerfacez.com/v1/set/"+stream;
// return "http://127.0.0.1/twitch/ffz_v1_set_"+stream;
}
} else {
return "http://api.frankerfacez.com/v1/room/"+stream;
}
}
private void parseResult(Type type, String stream, String result) {
if (result == null) {
return;
}
if (type == Type.FEATURE_FRIDAY && stream == null) {
// Response of the first request having only the info which channel
handleFeatureFriday(result);
return;
}
// Determine whether these emotes should be global
final boolean global = type == Type.GLOBAL || type == Type.FEATURE_FRIDAY;
String globalText = global ? "global" : "local";
Set<Emoticon> emotes = new HashSet<>();
List<Usericon> usericons = new ArrayList<>();
// Parse depending on type
if (type == Type.GLOBAL) {
emotes = FrankerFaceZParsing.parseGlobalEmotes(result);
} else if (type == Type.ROOM) {
emotes = FrankerFaceZParsing.parseRoomEmotes(result);
Usericon modIcon = FrankerFaceZParsing.parseModIcon(result);
if (modIcon != null) {
usericons.add(modIcon);
}
} else if (type == Type.FEATURE_FRIDAY) {
emotes = FrankerFaceZParsing.parseSetEmotes(result, Emoticon.SubType.FEATURE_FRIDAY, null);
for (Emoticon emote : emotes) {
if (featureFridayChannel != null) {
emote.setStream(featureFridayChannel);
}
}
}
LOGGER.info("[FFZ] ("+stream+", "+globalText+"): "+emotes.size()+" emotes received.");
if (!usericons.isEmpty()) {
LOGGER.info("[FFZ] ("+stream+"): "+usericons.size()+" usericons received.");
}
// Package accordingly and send the result to the listener
EmoticonUpdate emotesUpdate;
if (type == Type.FEATURE_FRIDAY) {
emotesUpdate = new EmoticonUpdate(emotes, Emoticon.Type.FFZ,
Emoticon.SubType.FEATURE_FRIDAY, null);
} else {
emotesUpdate = new EmoticonUpdate(emotes);
}
listener.channelEmoticonsReceived(emotesUpdate);
// Return icons if mod icon was found (will be empty otherwise)
listener.usericonsReceived(usericons);
}
/**
* Parse event JSON, update variables and request appropriate emotes if
* feature friday was found.
*
* @param json
*/
private void handleFeatureFriday(String json) {
// Also updates featureFridayChannel field
int set = parseFeatureFriday(json);
if (set == -1) {
// No feature friday found
featureFridaySet = -1;
clearFeatureFridayEmotes();
LOGGER.info(String.format("[FFZ] No Feature Friday found: %s",
StringUtil.trim(StringUtil.removeLinebreakCharacters(
StringUtil.shortenTo(json, 100)))));
} else {
// Feature friday found
if (featureFridaySet != set) {
/**
* If set changed, clear current emotes. If set is still the
* same then it can be assumed that emotes haven't changed much,
* so removing them when the actual emotes request is returned
* should be enough.
*/
clearFeatureFridayEmotes();
}
featureFridaySet = set;
request(Type.FEATURE_FRIDAY, String.valueOf(set), true);
}
}
/**
* Parse event JSON and return feature friday emote set. Also updates the
* feature friday channel variable.
*
* @param json
* @return The feature friday FFZ emote set, or -1 if none was found or an
* error occured
*/
private int parseFeatureFriday(String json) {
try {
JSONParser parser = new JSONParser();
JSONObject root = (JSONObject)parser.parse(json);
int set = ((Number)root.get("set")).intValue();
featureFridayChannel = (String)root.get("channel");
return set;
} catch (ParseException | NullPointerException | ClassCastException ex) {
// Assume no feature friday
}
featureFridayChannel = null;
return -1;
}
/**
* Send a message to the listener to clear all FFZ Feature Friday emotes.
*/
private void clearFeatureFridayEmotes() {
listener.channelEmoticonsReceived(new EmoticonUpdate(null,
Emoticon.Type.FFZ,
Emoticon.SubType.FEATURE_FRIDAY,
null));
}
/**
* Request and parse FFZ bot names.
*/
public void requestBotNames() {
UrlRequest request = new UrlRequest("https://api.frankerfacez.com/v1/badge/bot") {
@Override
public void requestResult(String result, int responseCode) {
if (result != null && responseCode == 200) {
Set<String> botNames = FrankerFaceZParsing.getBotNames(result);
LOGGER.info("[FFZ Bots] Found "+botNames.size()+" names");
listener.botNamesReceived(botNames);
}
}
};
request.setLabel("[FFZ Bots]");
new Thread(request).start();
}
}