package util;
import face.FaceManager;
import gui.forms.GUIMain;
import irc.account.OAuth;
import lib.JSON.JSONArray;
import lib.JSON.JSONObject;
import util.settings.Settings;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by Nick on 5/22/2015.
*/
public class APIRequests {
//Anything Twitch
public static class Twitch {
private static ConcurrentHashMap<String, String> usersIDMap = new ConcurrentHashMap<>();
private static final String TWITCH_API = "https://api.twitch.tv/kraken";
private static final String API_VERSION = "api_version=5"; // TODO removeme when the default is v5
public static final String CLIENT_ID = "client_id=qw8d3ve921t0n6e3if07l664f1jn1y7";
/**
* @param emotes The emoteset String that twitch provides in IRC.
* @return The JSON string of the set of emotes, otherwise an empty string.
*/
public static String getEmoteSet(String emotes)
{
return Utils.createAndParseBufferedReader(TWITCH_API + "/chat/emoticon_images?emotesets=" + emotes + "&" + CLIENT_ID);
}
/**
* @return The JSON string of every Twitch emote.
*/
public static String getAllEmotes()
{
return Utils.createAndParseBufferedReader(TWITCH_API + "/chat/emoticon_images?" + CLIENT_ID);
}
/**
* Since Twitch is eventually going to deprecate v3, we need to store a user and their corresponding ID. This
* method is in charge of obtaining said ID.
*
* @param channel The channel's name.
* @return The String of the ID for the channel.
*/
public static String getChannelID(String channel)
{
if (usersIDMap.contains(channel))
return usersIDMap.get(channel);
try
{
URL toRead = new URL(TWITCH_API + "/users?login=" + channel + "&" + CLIENT_ID + "&" + API_VERSION);
String line = Utils.createAndParseBufferedReader(toRead.openStream());
if (!line.isEmpty())
{
JSONObject init = new JSONObject(line);
if (init.getInt("_total") > 0)
{
JSONArray users = init.getJSONArray("users");
JSONObject user = users.getJSONObject(0);
String ID = user.getString("_id");
usersIDMap.put(channel, ID);
return ID;
}
}
} catch (Exception e)
{
GUIMain.log(e);
}
return "";
}
/**
* Fetches and downloads the given channel's subscriber icon.
*
* @param channel The channel to download the sub icon for.
* @return The path to the downloaded sub icon if successful, otherwise null.
*/
public static String getSubIcon(String channel)
{
try
{
channel = channel.replaceAll("#", "");
String ID = getChannelID(channel);
URL toRead = new URL(TWITCH_API + "/chat/" + ID + "/badges?" + CLIENT_ID + "&" + API_VERSION);
String line = Utils.createAndParseBufferedReader(toRead.openStream());
if (!line.isEmpty())
{
JSONObject init = new JSONObject(line);
if (init.has("subscriber"))
{
JSONObject sub = init.getJSONObject("subscriber");
if (!sub.getString("image").equalsIgnoreCase("null"))
{
return FaceManager.downloadIcon(sub.getString("image"), channel);
}
}
}
} catch (Exception e)
{
GUIMain.log(e);
}
return null;
}
/**
* Gets stream uptime.
*
* @return the current stream uptime.
*/
public static Response getUptimeString(String channelName)
{
if (channelName.contains("#")) channelName = channelName.replaceAll("#", "");
Response toReturn = new Response();
try
{
String ID = getChannelID(channelName);
URL uptime = new URL(TWITCH_API + "/streams/" + ID + "?" + CLIENT_ID + "&" + API_VERSION);
String line = Utils.createAndParseBufferedReader(uptime.openStream());
if (!line.isEmpty()) {
JSONObject outer = new JSONObject(line);
if (!outer.isNull("stream")) {
JSONObject stream = outer.getJSONObject("stream");
Instant started = Instant.parse(stream.getString("created_at"));
Duration duration = Duration.between(started, Instant.now());
int hours = (int) duration.abs().getSeconds() / 3600;
int mins = ((int) duration.abs().getSeconds() % 3600) / 60;
toReturn.wasSuccessful();
toReturn.setResponseText("The stream has been live for: " +
((hours > 0 ? (hours + " hours ") : "") + mins + " minutes"));
} else {
toReturn.setResponseText("The stream is not live!");
}
}
} catch (Exception e) {
toReturn.setResponseText("Error checking uptime due to Exception!");
GUIMain.log(e);
}
return toReturn;
}
/**
* Checks a channel to see if it's live (streaming).
*
* @param channelName The name of the channel to check.
* @return true if the specified channel is live and streaming, else false.
*/
public static boolean isChannelLive(String channelName) {
boolean isLive = false;
try
{
URL twitch = new URL(TWITCH_API + "/streams/" + channelName + "?" + CLIENT_ID);
String line = Utils.createAndParseBufferedReader(twitch.openStream());
if (!line.isEmpty()) {
JSONObject jsonObject = new JSONObject(line);
isLive = !jsonObject.isNull("stream") && !jsonObject.getJSONObject("stream").isNull("preview");
}
} catch (Exception ignored) {
}
return isLive;
}
/**
* Gets the amount of viewers for a channel.
*
* @param channelName The name of the channel to check.
* @return The int amount of viewers watching the given channel.
*/
public static int countViewers(String channelName) {
int count = -1;
try {//this could be parsed with JSON, but patterns work, and if it ain't broke...
URL twitch = new URL(TWITCH_API + "/streams/" + channelName + "?" + CLIENT_ID);
String line = Utils.createAndParseBufferedReader(twitch.openStream());
if (!line.isEmpty()) {
Matcher m = Constants.viewerTwitchPattern.matcher(line);
if (m.find()) {
try {
count = Integer.parseInt(m.group(1));
} catch (Exception ignored) {
}//bad Int parsing
}
}
} catch (Exception e) {
count = -1;
}
return count;
}
/**
* Gets the status of a channel, which is the title and game of the stream.
*
* @param channel The channel to get the status of.
* @return A string array with the status as first index and game as second.
*/
public static String[] getStatusOfStream(String channel) {
String[] toRet = {"", ""};
try {
if (channel.contains("#")) channel = channel.replaceAll("#", "");
URL twitch = new URL(TWITCH_API + "/channels/" + channel + "?" + CLIENT_ID);
String line = Utils.createAndParseBufferedReader(twitch.openStream());
if (!line.isEmpty()) {
JSONObject base = new JSONObject(line);
//these are never null, just blank strings at worst
toRet[0] = base.getString("status");
toRet[1] = base.getString("game");
}
} catch (Exception e) {
GUIMain.log("Failed to get status of stream due to Exception: ");
GUIMain.log(e);
}
return toRet;
}
/**
* Gets the title of a given channel.
*
* @param channel The channel to get the title of.
* @return The title of the stream.
*/
public static String getTitleOfStream(String channel) {
String[] status = getStatusOfStream(channel);
return status[0];
}
/**
* Gets the game of a given channel.
*
* @param channel The channel to get the game of.
* @return An empty string if not playing, otherwise the game being played.
*/
public static String getGameOfStream(String channel) {
String[] status = getStatusOfStream(channel);
return status[1];
}
/**
* Updates the stream's status to a given parameter.
*
* @param key The oauth key which MUST be authorized to edit the status of the stream.
* @param channel The channel to edit.
* @param message The message containing the new title/game to update to.
* @param isTitle If the change is for the title or game.
* @return The response Botnak has for the method.
*/
public static Response setStreamStatus(OAuth key, String channel, String message, boolean isTitle) {
Response toReturn = new Response();
if (key.canSetTitle()) {
String add = isTitle ? "title" : "game";
if (message.split(" ").length > 1) {
String toChangeTo = message.substring(message.indexOf(' ') + 1);
if (toChangeTo.equals(" ") || toChangeTo.equals("null")) toChangeTo = "";
if (toChangeTo.equalsIgnoreCase(isTitle ? getTitleOfStream(channel) : getGameOfStream(channel))) {
toReturn.setResponseText("Failed to set " + add + ", the " + add + " is already set to that!");
} else {
Response status = setStatusOfStream(key.getKey(), channel,
isTitle ? toChangeTo : getTitleOfStream(channel),
isTitle ? getGameOfStream(channel) : toChangeTo);
if (status.isSuccessful()) {
toReturn.wasSuccessful();
toChangeTo = "".equals(toChangeTo) ? (isTitle ? "(untitled broadcast)" : "(not playing a game)") : toChangeTo;
toReturn.setResponseText("Successfully set " + add + " to: \"" + toChangeTo + "\" !");
} else {
toReturn.setResponseText(status.getResponseText());
}
}
} else {
toReturn.setResponseText("Failed to set status status of the " + add + ", usage: !set" + add + " (new " + add + ") or \"null\"");
}
} else {
toReturn.setResponseText("This OAuth key cannot update the status of the stream! Try re-authenticating in the Settings GUI!");
}
return toReturn;
}
/**
* Sets the status of a stream.
*
* @param key The oauth key which MUST be authorized to edit the status of a stream.
* @param channel The channel to edit.
* @param title The title to set.
* @param game The game to set.
* @return The response Botnak has for the method.
*/
public static Response setStatusOfStream(String key, String channel, String title, String game) {
Response toReturn = new Response();
try {
if (channel.contains("#")) channel = channel.replace("#", "");
String request = TWITCH_API + "/channels/" + channel +
"?channel[status]=" + URLEncoder.encode(title, "UTF-8") +
"&channel[game]=" + URLEncoder.encode(game, "UTF-8") +
"&oauth_token=" + key.split(":")[1] + "&_method=put&" + CLIENT_ID;
URL twitch = new URL(request);
String line = Utils.createAndParseBufferedReader(twitch.openStream());
if (!line.isEmpty() && line.contains(title) && line.contains(game)) {
toReturn.wasSuccessful();
}
} catch (Exception e) {
GUIMain.log("Failed to update status due to Exception: ");
GUIMain.log(e);
toReturn.setResponseText("Failed to update status due to Exception!");
}
return toReturn;
}
/**
* Plays an ad on stream.
*
* @param key The oauth key which MUST be authorized to play a commercial on a stream.
* @param channel The channel to play the ad for.
* @param length How long
* @return true if the commercial played, else false.
*/
public static boolean playAdvert(String key, String channel, int length) {
boolean toReturn = false;
try {
length = Utils.capNumber(30, 180, length);//can't be longer than 3 mins/shorter than 30 sec
if ((length % 30) != 0) length = 30;//has to be divisible by 30 seconds
if (channel.contains("#")) channel = channel.replace("#", "");
String request = TWITCH_API + "/channels/" + channel + "/commercial";
URL twitch = new URL(request);
HttpURLConnection c = (HttpURLConnection) twitch.openConnection();
c.setRequestMethod("POST");
c.setDoOutput(true);
String toWrite = "length=" + length;
c.setRequestProperty("Client-ID", CLIENT_ID.split("=")[1]);
c.setRequestProperty("Authorization", "OAuth " + key.split(":")[1]);
c.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
c.setRequestProperty("Content-Length", String.valueOf(toWrite.length()));
OutputStreamWriter op = new OutputStreamWriter(c.getOutputStream());
op.write(toWrite);
op.close();
try {
int response = c.getResponseCode();
toReturn = (response == 204);
} catch (Exception e) {
GUIMain.log("Failed to get response code due to Exception: ");
GUIMain.log(e);
}
c.disconnect();
} catch (Exception e) {
GUIMain.log("Failed to play advertisement due to Exception: ");
GUIMain.log(e);
}
return toReturn;
}
/**
* Obtains the title and author of a video on Twitch.
*
* @param URL The URL to the video.
* @return The appropriate response.
*/
public static Response getTitleOfVOD(String URL) {
Response toReturn = new Response();
try {
String ID = "";
Pattern p = Pattern.compile("/[vcb]/([^&?/]+)");
Matcher m = p.matcher(URL);
if (m.find()) {
ID = m.group().replaceAll("/", "");
}
URL request = new URL(TWITCH_API + "/videos/" + ID + "?" + CLIENT_ID);
String line = Utils.createAndParseBufferedReader(request.openStream());
if (!line.isEmpty()) {
JSONObject init = new JSONObject(line);
String title = init.getString("title");
JSONObject channel = init.getJSONObject("channel");
String author = channel.getString("display_name");
toReturn.wasSuccessful();
toReturn.setResponseText("Linked Twitch VOD: \"" + title + "\" by " + author);
}
} catch (Exception e) {
toReturn.setResponseText("Failed to parse Twitch VOD due to an Exception!");
GUIMain.log(e);
}
return toReturn;
}
/**
* Uses the main account's OAuth to check for your followed channels, if enabled.
*
* @param key The oauth key to use.
* @return An array of live streams (empty if no one is live).
*/
public static ArrayList<String> getLiveFollowedChannels(String key) {
ArrayList<String> toReturn = new ArrayList<>();
try
{
URL request = new URL(TWITCH_API + "/streams/followed?oauth_token=" + key + "&limit=100&" + CLIENT_ID);
String line = Utils.createAndParseBufferedReader(request.openStream());
if (!line.isEmpty()) {
JSONObject init = new JSONObject(line);
JSONArray streams = init.getJSONArray("streams");
for (int i = 0; i < streams.length(); i++) {
JSONObject stream = streams.getJSONObject(i);
JSONObject channel = stream.getJSONObject("channel");
toReturn.add(channel.getString("name").toLowerCase());
}
}
} catch (Exception e) {
if (!e.getMessage().contains("401") && !e.getMessage().contains("503")) {
GUIMain.log("Failed to get live followed channels due to exception:");
GUIMain.log(e);
}
}
return toReturn;
}
/**
* Gets username suggestions when adding a stream.
*
* @param partial The partially typed text to prompt a suggestion.
* @return An array of suggested stream names.
*/
public static String[] getUsernameSuggestions(String partial) {
ArrayList<String> toReturn = new ArrayList<>();
try
{
URL request = new URL(TWITCH_API + "/search/channels?limit=10&query=" + partial + "&" + CLIENT_ID);
String line = Utils.createAndParseBufferedReader(request.openStream());
if (!line.isEmpty()) {
JSONObject init = new JSONObject(line);
JSONArray channels = init.getJSONArray("channels");
for (int i = 0; i < channels.length(); i++) {
JSONObject channel = channels.getJSONObject(i);
toReturn.add(channel.getString("name"));
}
}
} catch (Exception e) {
GUIMain.log(e);
}
return toReturn.toArray(new String[toReturn.size()]);
}
/**
* Gets the last 20 followers of a channel.
*
* @param channel The channel to check.
* @return A string array of (up to) the last 20 followers.
*/
public static String[] getLast20Followers(String channel) {
ArrayList<String> toReturn = new ArrayList<>();
try
{
URL request = new URL(TWITCH_API + "/channels/" + channel + "/follows?limit=20&" + CLIENT_ID);
String line = Utils.createAndParseBufferedReader(request.openStream());
if (!line.isEmpty()) {
JSONObject init = new JSONObject(line);
JSONArray follows = init.getJSONArray("follows");
for (int i = 0; i < follows.length(); i++) {
JSONObject person = follows.getJSONObject(i);
JSONObject user = person.getJSONObject("user");
toReturn.add(user.getString("name"));
}
}
} catch (Exception e) {
GUIMain.log(e);
}
return toReturn.toArray(new String[toReturn.size()]);
}
}
//Current playing song
public static class LastFM {
/**
* Gets the currently playing song from LastFM, assuming the LastFM account was set up correctly.
*
* @return The name of the song, else an empty string.
*/
public static Response getCurrentlyPlaying() {
Response toReturn = new Response();
if ("".equals(Settings.lastFMAccount.getValue())) {
toReturn.setResponseText("Failed to fetch current playing song, the user has no last.fm account set!");
return toReturn;
}
//TODO check the song requests engine to see if that is currently playing
String tracks_url = "http://www.last.fm/user/" + Settings.lastFMAccount.getValue() + "/now";
try {
URL request = new URL("http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=" +
Settings.lastFMAccount.getValue() + "&api_key=e0d3467ebb54bb110787dd3d77705e1a&format=json");
String line = Utils.createAndParseBufferedReader(request.openStream());
JSONObject outermost = new JSONObject(line);
JSONObject recentTracks = outermost.getJSONObject("recenttracks");
JSONArray songsArray = recentTracks.getJSONArray("track");
if (songsArray.length() > 0) {
JSONObject mostRecent = songsArray.getJSONObject(0);
JSONObject artist = mostRecent.getJSONObject("artist");
String artistOfSong = artist.getString("#text");
String nameOfSong = mostRecent.getString("name");
if (mostRecent.has("@attr")) {//it's the current song
toReturn.setResponseText("The current song is: " + artistOfSong + " - " + nameOfSong + " || " + tracks_url);
} else {
toReturn.setResponseText("The most recent song was: " + artistOfSong + " - " + nameOfSong + " || " + tracks_url);
}
toReturn.wasSuccessful();
} else {
toReturn.setResponseText("Failed to fetch current song; last.fm shows no recent tracks!");
}
} catch (Exception e) {
toReturn.setResponseText("Failed to fetch current playing song due to an Exception!");
GUIMain.log(e);
}
return toReturn;
}
}
//Youtube video data
public static class YouTube {
/**
* Fetches the title, author, and duration of a linked YouTube video.
*
* @param fullURL The URL to the video.
* @return The appropriate response.
*/
public static Response getVideoData(String fullURL) {
Response toReturn = new Response();
try {
Pattern p = null;
if (fullURL.contains("youtu.be/")) {
p = Pattern.compile("youtu\\.be/([^&?/]+)");
} else if (fullURL.contains("v=")) {
p = Pattern.compile("v=([^&?/]+)");
} else if (fullURL.contains("/embed/")) {
p = Pattern.compile("youtube\\.com/embed/([^&?/]+)");
}
if (p == null) {
toReturn.setResponseText("Could not read YouTube URL!");
return toReturn;
}
Matcher m = p.matcher(fullURL);
if (m.find()) {
String ID = m.group(1);
URL request = new URL("https://www.googleapis.com/youtube/v3/videos?id=" + ID +
"&part=snippet,contentDetails&key=AIzaSyDVKqwiK_VGelKlNCHtEFWFbDfVuzl9Q8c" +
"&fields=items(snippet(title,channelTitle),contentDetails(duration))");
String line = Utils.createAndParseBufferedReader(request.openStream());
if (!line.isEmpty()) {
JSONObject initial = new JSONObject(line);
JSONArray items = initial.getJSONArray("items");
if (items.length() < 1) {
toReturn.setResponseText("Failed to parse YouTube video! Perhaps a bad ID?");
return toReturn;
}
JSONObject juicyDetails = items.getJSONObject(0);
JSONObject titleAndChannel = juicyDetails.getJSONObject("snippet");
JSONObject duration = juicyDetails.getJSONObject("contentDetails");
String title = titleAndChannel.getString("title");
String channelName = titleAndChannel.getString("channelTitle");
Duration d = Duration.parse(duration.getString("duration"));
String time = getTimeString(d);
toReturn.setResponseText("Linked YouTube Video: \"" + title + "\" by " + channelName + " [" + time + "]");
toReturn.wasSuccessful();
}
}
} catch (Exception e) {
toReturn.setResponseText("Failed to parse YouTube video due to an Exception!");
}
return toReturn;
}
private static String getTimeString(Duration d) {
int s = (int) d.getSeconds();
int hours = s / 3600;
int minutes = (s % 3600) / 60;
int seconds = (s % 60);
if (hours > 0) return String.format("%d:%02d:%02d", hours, minutes, seconds);
else return String.format("%02d:%02d", minutes, seconds);
}
}
//URL Un-shortening
public static class UnshortenIt {
/**
* Fetches the domain of the shortened URL's un-shortened destination.
*
* @param shortenedURL The shortened URL string.
* @return The appropriate response.
*/
public static Response getUnshortened(String shortenedURL) {
Response toReturn = new Response();
toReturn.setResponseText("Failed to un-shorten URL! Click with caution!");
try {
URL request = new URL("http://urlex.org/txt/" + shortenedURL);
String line = Utils.createAndParseBufferedReader(request.openStream());
if (!line.isEmpty()) {
if (!line.equals(shortenedURL)) {
line = getHost(line).replaceAll("\\.", ",");
toReturn.setResponseText("Linked Shortened URL directs to: " + line + " !");
} else {
toReturn.setResponseText("Invalid shortened URL!");
}
}
} catch (Exception ignored) {
}
return toReturn;
}
private static String getHost(String webURL) {
String toReturn = webURL;
try {
URL url = new URL(webURL);
toReturn = url.getHost();
} catch (Exception ignored) {
}
return toReturn;
}
}
}