diff --git a/README.md b/README.md index bfdfd57a2..b1b5819ae 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,12 @@ External Libraries/Resources (for the license text see the APACHE_LICENSE file or http://www.apache.org/licenses/LICENSE-2.0). +* Tyrus: + * Files: `assets/lib/tyrus-standalone-client-1.12.jar` + * Website: https://tyrus.java.net + * License: "CDDL 1.1" and "GPL 2 with CPE" + (see https://tyrus.java.net/license.html) + * Favorites Icon by Everaldo Coelho: * File: `star.png` * Source: https://www.iconfinder.com/icons/17999/bookmark_favorite_star_icon diff --git a/assets/readme.txt b/assets/readme.txt index 3101cb986..9db0ae583 100644 --- a/assets/readme.txt +++ b/assets/readme.txt @@ -89,6 +89,11 @@ External Libraries License: "Apache License 2.0" http://www.apache.org/licenses/LICENSE-2.0 +* Tyrus [lib/tyrus-standalone-client-1.12.jar] + Website: https://tyrus.java.net + License: "CDDL 1.1" and "GPL 2 with CPE" + https://tyrus.java.net/license.html + * Favorites Icon [star.png] by Everaldo Coelho Source: https://www.iconfinder.com/icons/17999/bookmark_favorite_star_icon License: LGPL diff --git a/src/chatty/AddressManager.java b/src/chatty/AddressManager.java index 5e4c08265..f5ca527d4 100644 --- a/src/chatty/AddressManager.java +++ b/src/chatty/AddressManager.java @@ -8,6 +8,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.logging.Level; import java.util.logging.Logger; /** @@ -116,4 +117,19 @@ public static List parsePorts(String ports) { return parsedPorts; } + // Testing stuff + public static void main(String[] args) { + AddressManager m = new AddressManager(); + try { + for (int i=0;i<60;i++) { + InetSocketAddress addr = m.getAddress("irc.chat.twitch.tv", "6697,6667,443,80"); + System.out.println(addr); + m.addError(addr); + } + + } catch (UnknownHostException ex) { + Logger.getLogger(AddressManager.class.getName()).log(Level.SEVERE, null, ex); + } + } + } diff --git a/src/chatty/Chatty.java b/src/chatty/Chatty.java index bca3b0759..94b5cbbe2 100644 --- a/src/chatty/Chatty.java +++ b/src/chatty/Chatty.java @@ -50,9 +50,11 @@ public class Chatty { public static final String REDIRECT_URI = "http://127.0.0.1:61324/token/"; /** - * Version number of this version of Chatty + * Version number of this version of Chatty, consisting of numbers seperated + * by points. May contain a single "b" for beta versions, anything following + * it will be ignored for version checking. */ - public static final String VERSION = "0.8.3b"; + public static final String VERSION = "0.8.3b2"; /** * Enable Version Checker (if you compile and distribute this yourself, you diff --git a/src/chatty/TwitchClient.java b/src/chatty/TwitchClient.java index f5bf82aff..4c2900b94 100644 --- a/src/chatty/TwitchClient.java +++ b/src/chatty/TwitchClient.java @@ -904,6 +904,9 @@ else if (command.equals("ffz")) { else if (command.equals("ffzglobal")) { commandFFZ(null); } + else if (command.equals("ffzws")) { + g.printSystem("[FFZ-WS] Status: "+frankerFaceZ.getWsStatus()); + } else if (command.equals("refresh")) { commandRefresh(channel, parameter); } @@ -1832,6 +1835,11 @@ public void botNamesReceived(Set botNames) { botNameManager.addBotNames(null, botNames); } } + + @Override + public void wsInfo(String info) { + g.printDebugFFZ(info); + } } /** diff --git a/src/chatty/Version.java b/src/chatty/Version.java index f46d52c67..dedf49cc6 100644 --- a/src/chatty/Version.java +++ b/src/chatty/Version.java @@ -60,8 +60,8 @@ private void versionReceived(String newVersion) { String currentVersion = VERSION; // Check if current is beta version boolean isBetaVersion = false; - if (currentVersion.endsWith("b")) { - currentVersion = currentVersion.substring(0, currentVersion.length() - 1); + if (currentVersion.contains("b")) { + currentVersion = currentVersion.substring(0, currentVersion.indexOf("b")); isBetaVersion = true; } int compare = compareVersions(currentVersion, newVersion); diff --git a/src/chatty/gui/MainGui.java b/src/chatty/gui/MainGui.java index 628548f11..f8e6c5292 100644 --- a/src/chatty/gui/MainGui.java +++ b/src/chatty/gui/MainGui.java @@ -2724,6 +2724,16 @@ public void run() { } } + public void printDebugFFZ(final String line) { + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + debugWindow.printLineFFZ(line); + } + }); + } + /** * Outputs a line to the debug window * diff --git a/src/chatty/gui/components/DebugWindow.java b/src/chatty/gui/components/DebugWindow.java index 04786a797..62b94da52 100644 --- a/src/chatty/gui/components/DebugWindow.java +++ b/src/chatty/gui/components/DebugWindow.java @@ -26,6 +26,7 @@ public class DebugWindow extends JFrame { private final JCheckBox logIrc = new JCheckBox("Irc log", false); private final JTextArea text; private final JTextArea textIrcLog; + private final JTextArea textFFZLog; public DebugWindow(ItemListener listener) { setTitle("Debug"); @@ -50,10 +51,17 @@ public DebugWindow(ItemListener listener) { textIrcLog.setCaret(caret); JScrollPane scrollIrcLog = new JScrollPane(textIrcLog); + // Irc log + textFFZLog = new JTextArea(); + textFFZLog.setEditable(false); + textFFZLog.setCaret(caret); + JScrollPane scrollFFZLog = new JScrollPane(textFFZLog); + // Tabs JTabbedPane tabs = new JTabbedPane(); tabs.addTab("Log", scroll); tabs.addTab("Irc log", scrollIrcLog); + tabs.addTab("FFZ-WS", scrollFFZLog); // Settings (Checkboxes) logIrc.setToolTipText("Logging IRC traffic can reduce performance"); @@ -80,6 +88,10 @@ public void printLineIrc(String line) { printLine(textIrcLog, line); } + public void printLineFFZ(String line) { + printLine(textFFZLog, line); + } + private void printLine(JTextArea text, String line) { try { Document doc = text.getDocument(); diff --git a/src/chatty/gui/components/EmotesDialog.java b/src/chatty/gui/components/EmotesDialog.java index ddb47fc34..b678e56c3 100644 --- a/src/chatty/gui/components/EmotesDialog.java +++ b/src/chatty/gui/components/EmotesDialog.java @@ -701,33 +701,35 @@ protected void updateEmotes() { } else { // FFZ/BTTV Set channelEmotes = emoteManager.getEmoticons(stream); + + // Split Event/Regular emotes into separate structures Set regular = new HashSet<>(); Map> event = new HashMap<>(); - for (Emoticon emote : channelEmotes) { if (emote.type == Emoticon.Type.FFZ && emote.subType == Emoticon.SubType.EVENT) { - String info = emote.info; - if (!event.containsKey(info)) { - event.put(info, new HashSet()); + for (String info : emote.getInfos()) { + if (!event.containsKey(info)) { + event.put(info, new HashSet()); + } + event.get(info).add(emote); } - event.get(info).add(emote); - } - else { + } else { regular.add(emote); } } + if (channelEmotes.isEmpty()) { addTitle("No emotes found for #" + stream); addSubtitle("No FFZ or BTTV emotes found.", false); } else { addEmotes(regular, "Emotes specific to #" + stream); for (String info : event.keySet()) { - addEmotes(event.get(info), "Featured "+info); + addEmotes(event.get(info), "Featured " + info); } } - // Subemotes + // Subscriber Emotes int emoteset = emoteManager.getEmotesetFromStream(stream); if (emoteset != -1) { Set subEmotes = emoteManager.getEmoticons(emoteset); @@ -833,7 +835,8 @@ protected void updateEmotes() { lgbc.insets = new Insets(4, 4, 4, 4); addInfo(panel2, "Code:", emote.code); - addInfo(panel2, "Type:", emote.type.toString()); + String featured = emote.subType == Emoticon.SubType.EVENT ? " (Featured)" : ""; + addInfo(panel2, "Type:", emote.type.toString()+featured); if (emote.numericId > Emoticon.ID_UNDEFINED) { addInfo(panel2, "Emote ID:", ""+emote.numericId); } @@ -855,8 +858,11 @@ protected void updateEmotes() { if (emote.creator != null) { addInfo(panel2, "Emote by:", emote.creator); } - if (emote.info != null) { - addInfo(panel2, "Info:", emote.info); + + // Info + featured = emote.subType == Emoticon.SubType.EVENT ? "Featured " : ""; + for (String info : emote.getInfos()) { + addInfo(panel2, featured+info); } gbc.fill = GridBagConstraints.HORIZONTAL; @@ -886,6 +892,13 @@ private void addScaledEmote(Emoticon emote, JPanel panel, float scale, String la lgbc.gridx++; } + /** + * Adds a info line with separated key and value. + * + * @param panel + * @param key + * @param value + */ private void addInfo(JPanel panel, String key, String value) { lgbc.gridx = 0; lgbc.anchor = GridBagConstraints.WEST; @@ -899,6 +912,23 @@ private void addInfo(JPanel panel, String key, String value) { lgbc.gridy++; } + /** + * Adds a full-width info line. + * + * @param panel + * @param value + */ + private void addInfo(JPanel panel, String value) { + lgbc.gridx = 0; + lgbc.gridwidth = 2; + lgbc.anchor = GridBagConstraints.CENTER; + + panel.add(new JLabel(StringUtil.shortenTo(value, 35, 20)), lgbc); + + lgbc.gridwidth = 1; + lgbc.gridy++; + } + } } diff --git a/src/chatty/gui/components/help/help-guide_folders.html b/src/chatty/gui/components/help/help-guide_folders.html index 3da05d098..16615748f 100644 --- a/src/chatty/gui/components/help/help-guide_folders.html +++ b/src/chatty/gui/components/help/help-guide_folders.html @@ -192,15 +192,15 @@

Restore backup

  1. Prepare & Locate Backup Folder
      -
    • Make sure Chatty is not running. Settings are saved when - Chatty is closed, so if you change setting files manually - while it is running your manual changes would just be - overwritten once you close Chatty.
    • Enter /openBackupDir to open the Backup folder (or enter /dir and navigate to the Backup folder manually).
    • There should be several files in the format backup_x_<name>, these represent the separate batches of backups.
    • +
    • After that, make sure Chatty is not running. Settings are saved when + Chatty is closed, so if you change setting files manually + while it is running your manual changes would just be + overwritten once you close Chatty.
  2. Find latest backup and rename diff --git a/src/chatty/gui/components/help/help-setting_commands.html b/src/chatty/gui/components/help/help-setting_commands.html index 736536e1d..7e7a2e3a4 100644 --- a/src/chatty/gui/components/help/help-setting_commands.html +++ b/src/chatty/gui/components/help/help-setting_commands.html @@ -447,21 +447,21 @@

    Stream Highlights

    Channel name (with leading #) or empty empty Allows moderators in the given channel to run the - !addStreamHighlight command. + !highlight command. streamHighlightChannelRespond Boolean false If this is enabled, Chatty sends a message to chat when - a moderator uses the !addStreamHighlight command. Otherwise + a moderator uses the !hghlight command. Otherwise the response to the command is only shown locally. streamHighlightCommand String The command to use for moderators - !addstreamhighlight + !highlight Change this to define the command that can be used by mods to add stream highlights in the channel defined with the streamHighlightChannel setting. diff --git a/src/chatty/gui/components/help/help.html b/src/chatty/gui/components/help/help.html index 2b6997ebb..e32571114 100644 --- a/src/chatty/gui/components/help/help.html +++ b/src/chatty/gui/components/help/help.html @@ -5,7 +5,7 @@ -

    Chatty (Version: 0.8.3b1)

    +

    Chatty (Version: 0.8.3b2)

    @@ -1115,7 +1115,7 @@

    Allow your moderators to add highlights

    You can also let your moderators add stream highlights - (!addStreamHighlight [comment]), but you first have to + (!highlight [comment]), but you first have to change some settings:

      @@ -1297,6 +1297,8 @@

      the "Apache License 2.0".
    • Versions with Windows Global Hotkey support use JIntellitype, which is licensed under the "Apache License 2.0".
    • +
    • Using the Tyrus standalone websocket client library licensed under + CDDL 1.1 and GPL 2 with CPE.
    • Favorite Icon (Star Icon) by Everaldo Coelho under LGPL.
    • Several Icons from the Tango Icon Theme diff --git a/src/chatty/gui/components/menus/EmoteContextMenu.java b/src/chatty/gui/components/menus/EmoteContextMenu.java index 088f6d021..a487c0ff2 100644 --- a/src/chatty/gui/components/menus/EmoteContextMenu.java +++ b/src/chatty/gui/components/menus/EmoteContextMenu.java @@ -49,13 +49,18 @@ public EmoteContextMenu(EmoticonImage emoteImage, ContextMenuListener listener) } else if (emote.type == Emoticon.Type.CUSTOM) { addItem("", "Custom Emote"); } - if (emote.info != null) { - if (emote.subType == Emoticon.SubType.EVENT) { - addItem("", "Featured "+emote.info); - } else { - addItem("", emote.info); + + // Info + if (emote.subType == Emoticon.SubType.EVENT) { + for (String info : emote.getInfos()) { + addItem("", "Featured " + info); + } + } else { + for (String info : emote.getInfos()) { + addItem("", info); } } + addStreamSubmenu(emote); } diff --git a/src/chatty/util/JSONUtil.java b/src/chatty/util/JSONUtil.java index 598eaa2cd..914b136ad 100644 --- a/src/chatty/util/JSONUtil.java +++ b/src/chatty/util/JSONUtil.java @@ -52,9 +52,9 @@ public static boolean getBoolean(JSONObject data, Object key, boolean errorValue return errorValue; } - public static String listToJSON(String... args) { + public static String listToJSON(Object... args) { JSONArray o = new JSONArray(); - for (String a : args) { + for (Object a : args) { o.add(a); } return o.toJSONString(); diff --git a/src/chatty/util/api/Emoticon.java b/src/chatty/util/api/Emoticon.java index 6d2e1b4db..b3075bbe5 100644 --- a/src/chatty/util/api/Emoticon.java +++ b/src/chatty/util/api/Emoticon.java @@ -22,6 +22,7 @@ import java.util.HashSet; import java.util.Objects; import java.util.Set; +import java.util.TreeSet; import java.util.WeakHashMap; import java.util.concurrent.ExecutionException; import java.util.logging.Logger; @@ -106,10 +107,10 @@ public static enum SubType { public final String stringId; public final String urlX2; public final String creator; - public final String info; public final boolean isAnimated; private String stream; + private Set infos; private volatile int width; private volatile int height; @@ -137,11 +138,11 @@ public static class Builder { private boolean literal = false; private String stream; private Set streamRestrictions; + private Set infos; private int emoteset = SET_UNDEFINED; private int numericId = ID_UNDEFINED; private String stringId = null; private String creator; - private String info; private boolean isAnimated = false; public Builder(Type type, String search, String url) { @@ -201,8 +202,13 @@ public Builder setCreator(String creator) { return this; } - public Builder setInfo(String info) { - this.info = info; + public Builder addInfo(String info) { + if (info != null) { + if (infos == null) { + infos = new HashSet<>(); + } + infos.add(info); + } return this; } @@ -312,7 +318,7 @@ private Emoticon(Builder builder) { this.numericId = builder.numericId; this.stringId = builder.stringId; this.creator = builder.creator; - this.info = builder.info; + this.infos = builder.infos; this.isAnimated = builder.isAnimated; this.subType = builder.subtype; } @@ -384,6 +390,30 @@ public boolean hasStreamSet() { return stream != null; } + public synchronized void addInfos(Set infosToAdd) { + if (infos == null) { + infos = new HashSet<>(); + } + infos.addAll(infosToAdd); + } + + /** + * Creates a copy of the info strings. + * + *

      Info strings are intended to be displayed to the user along with other + * information about the emote. This is probably mainly going to be used for + * FFZ/BTTV emotes.

      + * + * @return A TreeSet of info strings (defensive copying), or an empty Set if + * no info strings are available + */ + public synchronized Set getInfos() { + if (infos == null) { + return new TreeSet<>(); + } + return new TreeSet<>(infos); + } + public boolean hasGlobalEmoteset() { return this.emoteSet == SET_GLOBAL || this.emoteSet == SET_UNDEFINED; } diff --git a/src/chatty/util/api/Emoticons.java b/src/chatty/util/api/Emoticons.java index d5adbe357..0c98c8d6f 100644 --- a/src/chatty/util/api/Emoticons.java +++ b/src/chatty/util/api/Emoticons.java @@ -920,7 +920,7 @@ private boolean loadCustomEmote(String line) { * @return The info text */ public String getCustomEmotesInfo() { - if (customEmotes.size() == 0) { + if (customEmotes.isEmpty()) { return "No custom emotes loaded"; } StringBuilder b = new StringBuilder(customEmotes.size()+" custom emotes loaded:\n"); diff --git a/src/chatty/util/ffz/FrankerFaceZ.java b/src/chatty/util/ffz/FrankerFaceZ.java index 0edb15309..f01fe426f 100644 --- a/src/chatty/util/ffz/FrankerFaceZ.java +++ b/src/chatty/util/ffz/FrankerFaceZ.java @@ -67,6 +67,10 @@ public void disconnectWs() { ws.disconnect(); } + public String getWsStatus() { + return ws.getStatus(); + } + public void joined(String room) { ws.addRoom(room); } diff --git a/src/chatty/util/ffz/FrankerFaceZListener.java b/src/chatty/util/ffz/FrankerFaceZListener.java index 455f5aeae..b7b891f91 100644 --- a/src/chatty/util/ffz/FrankerFaceZListener.java +++ b/src/chatty/util/ffz/FrankerFaceZListener.java @@ -14,4 +14,5 @@ public interface FrankerFaceZListener { public void channelEmoticonsReceived(EmoticonUpdate emotes); public void usericonsReceived(List icons); public void botNamesReceived(Set botNames); + public void wsInfo(String info); } diff --git a/src/chatty/util/ffz/FrankerFaceZParsing.java b/src/chatty/util/ffz/FrankerFaceZParsing.java index d2d59506c..c2dd43388 100644 --- a/src/chatty/util/ffz/FrankerFaceZParsing.java +++ b/src/chatty/util/ffz/FrankerFaceZParsing.java @@ -214,7 +214,7 @@ public static Emoticon parseEmote(JSONObject emote, String streamRestriction, b.setCreator(creator); b.setNumericId(id); b.addStreamRestriction(streamRestriction); - b.setInfo(info); + b.addInfo(info); b.setSubType(subType); return b.build(); } catch (ClassCastException | NullPointerException ex) { diff --git a/src/chatty/util/ffz/WebsocketClient.java b/src/chatty/util/ffz/WebsocketClient.java index 1f55ee880..223de1408 100644 --- a/src/chatty/util/ffz/WebsocketClient.java +++ b/src/chatty/util/ffz/WebsocketClient.java @@ -1,6 +1,7 @@ package chatty.util.ffz; +import chatty.util.DateTime; import static chatty.util.MiscUtil.getStackTrace; import chatty.util.SSLUtil; import chatty.util.ffz.WebsocketClient.MyConfigurator; @@ -9,6 +10,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; import java.util.logging.Logger; import javax.websocket.ClientEndpoint; import javax.websocket.ClientEndpointConfig; @@ -24,7 +26,8 @@ import org.glassfish.tyrus.client.SslEngineConfigurator; /** - * + * Maintain the connection and handle sending/receiving commands correctly. + * * @author tduva */ @ClientEndpoint(configurator = MyConfigurator.class) @@ -32,48 +35,126 @@ public class WebsocketClient { private final static Logger LOGGER = Logger.getLogger(WebsocketClient.class.getName()); - private volatile Session s; private final MessageHandler handler; + private volatile boolean requestedDisconnect; private int connectionAttempts; + private volatile boolean ssl; + private ClientManager clientManager; + private String[] servers; - private int commandCount; private boolean connecting; - + private volatile Session s; + private int commandCount; + private long timeConnected; + private long timeLastMessageReceived; + public WebsocketClient(MessageHandler handler) { this.handler = handler; } - public void connect(String server, String alternateServer) { + /** + * The way it is now, this will only work once, because this is intended to + * stay connected. + * + * @param servers + */ + public synchronized void connect(String[] servers) { if (connecting) { return; } connecting = true; - ClientManager client = ClientManager.createClient(); - try { - client.getProperties().put(ClientProperties.RECONNECT_HANDLER, new Reconnect()); - - try { - if (server.startsWith("wss://")) { - SslEngineConfigurator sslEngineConfigurator = new SslEngineConfigurator( - SSLUtil.getSSLContextWithLE(), true, false, false); - client.getProperties().put(ClientProperties.SSL_ENGINE_CONFIGURATOR, sslEngineConfigurator); - } - } catch (Exception ex) { - LOGGER.warning("Failed adding support for Lets Encrypt: "+ex); - if (alternateServer != null) { - server = alternateServer; - } + this.servers = servers; + new Thread(new Runnable() { + + @Override + public void run() { + prepareConnection(); + connectToRandomServer(); } + }).start(); + } + + /** + * Get Websocket status in text form, with some basic formatting. + * + * @return + */ + public synchronized String getStatus() { + if (!connecting) { + return "Not connected"; + } + if (s != null && s.isOpen()) { + return String.format("Connected for %s\n" + + "\tServer: %s\n" + + "\tCommands sent: %d\n" + + "\tLast message received: %s ago", + DateTime.ago(timeConnected), + s.getRequestURI(), + commandCount, + DateTime.ago(timeLastMessageReceived)); + } + return "Connecting.."; + } + + /** + * Create and configure a Client Manager. + */ + private void prepareConnection() { + clientManager = ClientManager.createClient(); + clientManager.getProperties().put(ClientProperties.RECONNECT_HANDLER, new Reconnect()); - client.asyncConnectToServer(this, new URI(server)); + // Try to add Let's Encrypt cert for SSL + try { + SslEngineConfigurator sslEngineConfigurator = new SslEngineConfigurator( + SSLUtil.getSSLContextWithLE(), true, false, false); + clientManager.getProperties().put(ClientProperties.SSL_ENGINE_CONFIGURATOR, sslEngineConfigurator); + ssl = true; + } catch (Exception ex) { + LOGGER.warning("Failed adding support for Lets Encrypt: " + ex); + ssl = false; + } + } + + /** + * Randomly select a server from the given list of servers. Prepend ws:// or + * wss:// depending on whether this should use SSL or not. + * + * @param servers Array of servers, without protocol prefix + * @param ssl + * @return The server, including protocol prefix + */ + private static String getRandomServer(String[] servers, boolean ssl) { + String server = servers[ThreadLocalRandom.current().nextInt(servers.length)]; + if (ssl) { + server = "wss://" + server; + } else { + server = "ws://" + server; + } + return server; + } + + private void connectToRandomServer() { + connect(getRandomServer(servers, ssl)); + } + + private void connect(String server) { + try { LOGGER.info("[FFZ-WS] Connecting to "+server); + clientManager.asyncConnectToServer(this, new URI(server)); } catch (Exception ex) { LOGGER.warning("[FFZ-WS] Error connecting: "+ex); } } - public void disonnect() { + /** + * Disconnect from the server. + * + * Currently connecting to the server only works once, since it's intended + * to stay connected all the time, so using this should only be done when + * the program is closed. + */ + public synchronized void disonnect() { try { requestedDisconnect = true; if (s != null) { @@ -99,10 +180,7 @@ public boolean onDisconnect(CloseReason closeReason) { @Override public boolean onConnectFailure(Exception exception) { if (requestedDisconnect) { - LOGGER.warning("[FFZ-WS] Cancelled reconnecting.."); - return false; - } - if (connectionAttempts > 30) { + LOGGER.info("[FFZ-WS] Cancelled reconnecting.."); return false; } connectionAttempts++; @@ -111,7 +189,18 @@ public boolean onConnectFailure(Exception exception) { getDelay(), exception, exception.getCause().toString())); - return true; + + // Reconnect manually, so that the server can be changed + new java.util.Timer().schedule( + new java.util.TimerTask() { + @Override + public void run() { + connectToRandomServer(); + } + }, + getDelay()*1000 + ); + return false; } @Override @@ -124,29 +213,50 @@ public static class MyConfigurator extends ClientEndpointConfig.Configurator { @Override public void beforeRequest(Map> headers) { - headers.put("Origin", Arrays.asList("www.twitch.tv")); + // Empty Origin, otherwise would default to server name + headers.put("Origin", Arrays.asList("")); } @Override public void afterResponse(HandshakeResponse hr) { - //process the handshake response } } + /** + * Send a message to the server. Does nothing if the connection is not open. + * + * @param text + */ public synchronized void send(String text) { if (s != null && s.isOpen()) { s.getAsyncRemote().sendText(text); System.out.println("SENT: "+text); + handler.handleSent(text); } } - public void sendCommand(String command, String param) { + /** + * Send a command to the server. Does nothing if the connection is not open. + * + *

      Automatically increases and prefixes the command counter.

      + * + * @param command + * @param param + */ + public synchronized void sendCommand(String command, String param) { if (s != null && s.isOpen()) { commandCount += 1; send(String.format("%d %s %s", commandCount, command, param)); } } + /** + * Handle message already parsed into id, command and parameters. + * + * @param id The message id + * @param command The command + * @param params The parameters + */ private void handleCommand(int id, String command, String params) { handler.handleCommand(id, command, params); if (command.equals("error")) { @@ -162,12 +272,14 @@ public synchronized void onOpen(Session session) { connectionAttempts = 0; LOGGER.info("[FFZ-WS] Connected"); handler.handleConnect(); + timeConnected = System.currentTimeMillis(); } @OnMessage public synchronized void onMessage(String message, Session session) { System.out.println("RECEIVED: " + message); - handler.handleMessage(message); + timeLastMessageReceived = System.currentTimeMillis(); + handler.handleReceived(message); try { String[] split = message.split(" ", 3); int id = Integer.parseInt(split[0]); @@ -194,7 +306,8 @@ public void onError(Session session, Throwable t) { } public static interface MessageHandler { - public void handleMessage(String text); + public void handleReceived(String text); + public void handleSent(String sent); public void handleCommand(int id, String command, String params); public void handleConnect(); } diff --git a/src/chatty/util/ffz/WebsocketManager.java b/src/chatty/util/ffz/WebsocketManager.java index 5e6803fa5..3eb1095a1 100644 --- a/src/chatty/util/ffz/WebsocketManager.java +++ b/src/chatty/util/ffz/WebsocketManager.java @@ -12,22 +12,23 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.concurrent.ThreadLocalRandom; -import java.util.logging.Level; import java.util.logging.Logger; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; /** - * + * FFZ Websocket Manager. Handling the more top-level stuff of parsing actual + * commands and maintaining the list of rooms to sub to. + * * @author tduva */ public class WebsocketManager { - private final static String VERSION = "chatty-"+Chatty.VERSION; private final static Logger LOGGER = Logger.getLogger(WebsocketManager.class.getName()); + private final static String VERSION = "chatty_"+Chatty.VERSION; + private final Set rooms = new HashSet<>(); private final Map> prevEmotesets = new HashMap<>(); private final WebsocketClient c; @@ -35,20 +36,20 @@ public class WebsocketManager { private final JSONParser parser = new JSONParser(); private final FrankerFaceZListener listener; - private long timeLastReceived; private long serverTimeOffset; private final Settings settings; - public WebsocketManager(FrankerFaceZListener listener, final Settings settings) { + public WebsocketManager(final FrankerFaceZListener listener, + final Settings settings) { this.listener = listener; this.settings = settings; c = new WebsocketClient(new WebsocketClient.MessageHandler() { @Override - public void handleMessage(String text) { - timeLastReceived = System.currentTimeMillis(); + public void handleReceived(String text) { + listener.wsInfo(">> "+text); } @Override @@ -65,41 +66,48 @@ public void handleCommand(int id, String command, String params) { @Override public void handleConnect() { - c.sendCommand("hello", JSONUtil.listToJSON(VERSION, "false")); + c.sendCommand("hello", JSONUtil.listToJSON(VERSION, false)); for (String room : rooms) { subRoom(room); } c.sendCommand("ready", "0"); } + + @Override + public void handleSent(String sent) { + listener.wsInfo("SENT: "+sent); + } }); } - private static String getServer() { - switch (ThreadLocalRandom.current().nextInt(4)) { - case 0: return "catbag.frankerfacez.com"; - case 1: return "andknuckles.frankerfacez.com"; - case 2: return "tuturu.frankerfacez.com"; - } - return "catbag.frankerfacez.com"; + private static String[] getServers() { + return new String[] { + "catbag.frankerfacez.com", + "andknuckles.frankerfacez.com", + "tuturu.frankerfacez.com" + }; } - public static void main(String[] args) { - System.out.println(getServer()); + public String getStatus() { + return c.getStatus(); } public synchronized void connect() { - // TODO: Disabled for now -// if (!settings.getBoolean("ffz") || !settings.getBoolean("ffzEvent")) { -// return; -// } -// String server = getServer(); -// c.connect("wss://"+server, "ws://"+server); + if (!settings.getBoolean("ffz") || !settings.getBoolean("ffzEvent")) { + return; + } + c.connect(getServers()); } public synchronized void disconnect() { c.disonnect(); } + /** + * Subscribe to a room. + * + * @param room + */ public synchronized void addRoom(String room) { if (!Helper.validateStream(room)) { return; @@ -111,6 +119,12 @@ public synchronized void addRoom(String room) { } } + /** + * Remove subscription to a room. This also removes all current FFZ EVENT + * emotes from that room. + * + * @param room + */ public synchronized void removeRoom(String room) { if (!Helper.validateStream(room)) { return; @@ -139,10 +153,16 @@ private void parseHelloResponse(String json) { serverTimeOffset = System.currentTimeMillis() - serverTime; LOGGER.info("[FFZ-WS] Server Time Offset: "+serverTimeOffset); } catch (Exception ex) { - + LOGGER.warning("Error parsing 'hello' response: "+ex); } } + /** + * Parses the "follow_sets" message from the server and updates the emotes + * accordingly, but only if the emotesets have changed. + * + * @param json + */ private void parseFollowsets(String json) { try { JSONObject data = (JSONObject) parser.parse(json); @@ -158,14 +178,32 @@ private void parseFollowsets(String json) { } } } catch (Exception ex) { - Logger.getLogger(WebsocketManager.class.getName()).log(Level.SEVERE, null, ex); + LOGGER.warning("[FFZ] Error parsing 'follow_sets': "+ex+" ["+json+"]"); } } + /** + * Fetches all emotes of the given emotesets, useable in the given room and + * sends them to the listener (removing previous EVENT emotes in that room). + * + * @param room The room the emotes will be useable in + * @param emotesets The set of FFZ emotesets + */ private void fetchEmotes(String room, Set emotesets) { Set result = new HashSet<>(); for (int set : emotesets) { - result.addAll(fetchEmoteSet(room, set)); + Set fetched = fetchEmoteSet(room, set); + for (Emoticon emoteToAdd : fetched) { + // Add info to already existing emotes + for (Emoticon emote : result) { + if (emote.equals(emoteToAdd)) { + emote.addInfos(emoteToAdd.getInfos()); + break; + } + } + // Add emote to result if not already added (Set) + result.add(emoteToAdd); + } } EmoticonUpdate update = new EmoticonUpdate(result, Emoticon.Type.FFZ, @@ -174,6 +212,12 @@ private void fetchEmotes(String room, Set emotesets) { listener.channelEmoticonsReceived(update); } + /** + * Sends an update to the listener to remove all FFZ EVENT emotes from the + * given room. + * + * @param room The channel the emotes should be removed from + */ private void removeEmotes(String room) { EmoticonUpdate update = new EmoticonUpdate(null, Emoticon.Type.FFZ, @@ -182,6 +226,13 @@ private void removeEmotes(String room) { listener.channelEmoticonsReceived(update); } + /** + * Get the emotes from a specific emoteset, useable in the given room. + * + * @param room The channel the emotes should be useable in + * @param emoteset The FFZ emoteset to fetch + * @return A set of emotes or an empty set if an error occured + */ private Set fetchEmoteSet(String room, int emoteset) { UrlRequest r = new UrlRequest("https://api.frankerfacez.com/v1/set/"+emoteset); r.run();