diff --git a/.idea/libraries/cedarsoftware_json_io.xml b/.idea/libraries/cedarsoftware_json_io.xml
new file mode 100644
index 0000000..9c8918e
--- /dev/null
+++ b/.idea/libraries/cedarsoftware_json_io.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/skunkworks.iml b/.idea/skunkworks.iml
index 89da5ad..8222202 100644
--- a/.idea/skunkworks.iml
+++ b/.idea/skunkworks.iml
@@ -11,5 +11,6 @@
+
\ No newline at end of file
diff --git a/com/danitheskunk/skunkworks/TestTwitch.java b/com/danitheskunk/skunkworks/TestTwitch.java
new file mode 100644
index 0000000..735d27b
--- /dev/null
+++ b/com/danitheskunk/skunkworks/TestTwitch.java
@@ -0,0 +1,33 @@
+package com.danitheskunk.skunkworks;
+
+import com.danitheskunk.skunkworks.net.*;
+
+public class TestTwitch extends BaseGame implements IIrcMessageHandler {
+ Twitch twitch;
+ TwitchChat chat;
+
+ public TestTwitch() {
+ super(new Vec2i(640, 360), "stuff");
+ twitch = new Twitch("thvs1beyj8w6ono2jqirdz7uuu62qu");
+ chat = twitch.connectChat();
+ var chan = chat.join("#danitheskunk");
+ chan.sendMessage("nyaa!");
+ chan.setMessageHandler(this::handleMessage);
+ }
+
+ @Override
+ public void handleMessage(IrcChannel channel, String message, IrcHostmask from) {
+ if(message.equals("!nya")) {
+ channel.sendMessage("*meows at " + from.getNick() + "*");
+ }
+ }
+
+ @Override
+ protected void update(double delta) {
+ chat.tick();
+ }
+
+ static public void main(String[] params) {
+ new TestTwitch().run();
+ }
+}
diff --git a/com/danitheskunk/skunkworks/net/HttpUtils.java b/com/danitheskunk/skunkworks/net/HttpUtils.java
new file mode 100644
index 0000000..39a64ef
--- /dev/null
+++ b/com/danitheskunk/skunkworks/net/HttpUtils.java
@@ -0,0 +1,119 @@
+package com.danitheskunk.skunkworks.net;
+
+import com.cedarsoftware.util.io.JsonReader;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.ProtocolException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class HttpUtils {
+ public static RequestBuilder request(String uriStr) {
+ return new RequestBuilder(uriStr);
+ }
+
+ public static class RequestBuilder {
+ private Map customHeader;
+ private Map queryParameters;
+ private String requestMethod;
+ private String urlStr;
+
+ private RequestBuilder(String urlStr) {
+ customHeader = new HashMap<>();
+ queryParameters = new HashMap<>();
+ this.urlStr = urlStr;
+ requestMethod = "GET";
+ }
+
+ private static String queryStrHelper(Map.Entry entry) {
+ return URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) +
+ "=" +
+ URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8);
+ }
+
+ public RequestBuilder header(String key, String value) {
+ customHeader.put(key, value);
+ return this;
+ }
+
+ public RequestBuilder queryParameter(String key, String value) {
+ queryParameters.put(key, value);
+ return this;
+ }
+
+ public RequestBuilder requestMethod(String method) {
+ requestMethod = method;
+ return this;
+ }
+
+ public String run() {
+ HttpURLConnection con;
+
+ var paramStr = "";
+ if(queryParameters.size() > 0) {
+ paramStr = "?" + queryParameters.entrySet().stream().map(
+ RequestBuilder::queryStrHelper).collect(Collectors.joining(
+ "&"));
+ }
+
+ try {
+ var url = new URL(urlStr + paramStr);
+ con = (HttpURLConnection) url.openConnection();
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ con.setRequestMethod(requestMethod);
+ } catch(ProtocolException e) {
+ throw new RuntimeException(e);
+ }
+
+ for(var entry : customHeader.entrySet()) {
+ con.setRequestProperty(entry.getKey(), entry.getValue());
+ //System.out.printf("%s: %s\n", entry.getKey(), entry.getValue());
+ }
+
+ BufferedReader in;
+ try {
+ in = new BufferedReader(new InputStreamReader(con.getInputStream()));
+ } catch(IOException e) {
+ String inputLine;
+ var ein = new BufferedReader(new InputStreamReader(con.getErrorStream()));
+ try {
+ while((inputLine = ein.readLine()) != null) {
+ System.err.println(inputLine);
+ }
+ } catch(IOException ex) {
+ throw new RuntimeException(ex);
+ } throw new RuntimeException(e);
+ }
+
+ String inputLine;
+ StringBuffer content = new StringBuffer();
+ try {
+ while((inputLine = in.readLine()) != null) {
+ content.append(inputLine);
+ }
+ in.close();
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ return content.toString();
+ }
+
+ public java.util.Map runJSON() {
+ return JsonReader.jsonToMaps(run());
+ }
+
+ }
+
+}
diff --git a/com/danitheskunk/skunkworks/net/IIrcMessageHandler.java b/com/danitheskunk/skunkworks/net/IIrcMessageHandler.java
new file mode 100644
index 0000000..fb4b516
--- /dev/null
+++ b/com/danitheskunk/skunkworks/net/IIrcMessageHandler.java
@@ -0,0 +1,5 @@
+package com.danitheskunk.skunkworks.net;
+
+public interface IIrcMessageHandler {
+ void handleMessage(IrcChannel channel, String message, IrcHostmask from);
+}
diff --git a/com/danitheskunk/skunkworks/net/IrcChannel.java b/com/danitheskunk/skunkworks/net/IrcChannel.java
new file mode 100644
index 0000000..fafebae
--- /dev/null
+++ b/com/danitheskunk/skunkworks/net/IrcChannel.java
@@ -0,0 +1,31 @@
+package com.danitheskunk.skunkworks.net;
+
+public class IrcChannel {
+ private IrcClient irc;
+ private String name;
+ private IIrcMessageHandler messageHandler;
+
+ public IrcChannel(IrcClient irc, String name) {
+ this.irc = irc;
+ this.name = name;
+ }
+
+ public void sendMessage(String message) {
+ irc.privmsg(name, message);
+ }
+
+ public void processMessage(String message, IrcHostmask from) {
+ System.out.printf("[%s] <%s> %s\n", name, from.getNick(), message);
+ if(messageHandler != null) {
+ messageHandler.handleMessage(this, message, from);
+ }
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setMessageHandler(IIrcMessageHandler messageHandler) {
+ this.messageHandler = messageHandler;
+ }
+}
diff --git a/com/danitheskunk/skunkworks/net/IrcClient.java b/com/danitheskunk/skunkworks/net/IrcClient.java
new file mode 100644
index 0000000..cffde99
--- /dev/null
+++ b/com/danitheskunk/skunkworks/net/IrcClient.java
@@ -0,0 +1,114 @@
+package com.danitheskunk.skunkworks.net;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Map;
+
+public class IrcClient {
+ private String buffer;
+ private Map channels;
+ private char[] chbuf;
+ private InputStreamReader in;
+ private BufferedWriter out;
+ private Socket sock;
+
+ public IrcClient(String hostname, int port, String nick) {
+ this(hostname, port, nick, null);
+ }
+
+ public IrcClient(String hostname, int port, String nick, String pass) {
+ buffer = "";
+ //todo: increase size
+ chbuf = new char[8];
+ channels = new HashMap<>();
+ try {
+ sock = new Socket(hostname, port);
+ in = new InputStreamReader(sock.getInputStream());
+ out = new BufferedWriter(new OutputStreamWriter(sock.getOutputStream()));
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ if(pass != null) {
+ send(new IrcMessage("PASS").addParam(pass));
+ }
+ send(new IrcMessage("NICK").addParam(nick));
+ }
+
+ public IrcChannel join(String channel) {
+ send(new IrcMessage("JOIN").addParam(channel));
+ var chan = new IrcChannel(this, channel);
+ channels.put(channel, chan);
+ return chan;
+ }
+
+ public void privmsg(String target, String msg) {
+ send(new IrcMessage("PRIVMSG").addParam(target).addParam(msg));
+ }
+
+ private void processMessage(IrcMessage msg) {
+ var cmd = msg.getCommand();
+ if(cmd.equals("PING")) {
+ send(new IrcMessage("PONG").addParam(msg.getParams().get(0)));
+ } else if(cmd.equals("PRIVMSG")) {
+ var params = msg.getParams();
+ var target = params.get(0);
+ var text = params.get(1);
+ if(channels.containsKey(target)) {
+ channels.get(target).processMessage(
+ text,
+ IrcHostmask.fromPrefix(msg.getPrefix())
+ );
+ } else {
+ System.out.printf("got privmsg for unknown target '%s'\n",
+ target
+ );
+ }
+ }
+ System.out.printf("IRC| %s", msg.toString());
+ }
+
+ public void run() {
+ while(true) {
+ tick();
+ try {
+ Thread.sleep(100);
+ } catch(InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private void send(IrcMessage msg) {
+ try {
+ out.write(msg.toString());
+ out.flush();
+ //System.out.print(msg.toString());
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void tick() {
+ try {
+ while(in.ready()) {
+ //System.out.println("reading");
+ int count = in.read(chbuf);
+ var sb = new StringBuilder();
+ sb.append(chbuf, 0, count);
+ buffer += sb.toString();
+ if(buffer.contains("\r\n")) {
+ var sp = buffer.split("\r\n");
+ buffer = sp.length == 1 ? "" : sp[1];
+ processMessage(IrcMessage.fromString(sp[0]));
+ }
+ }
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/com/danitheskunk/skunkworks/net/IrcHostmask.java b/com/danitheskunk/skunkworks/net/IrcHostmask.java
new file mode 100644
index 0000000..93c9f9e
--- /dev/null
+++ b/com/danitheskunk/skunkworks/net/IrcHostmask.java
@@ -0,0 +1,45 @@
+package com.danitheskunk.skunkworks.net;
+
+public class IrcHostmask {
+ private final String nick, user, host;
+
+ public IrcHostmask(String nick, String user, String host) {
+ this.nick = nick;
+ this.user = user;
+ this.host = host;
+ }
+
+ public static IrcHostmask fromPrefix(String str) {
+ String nick;
+ String user = "";
+ String host = "";
+
+ if(str.contains("@")) {
+ var sp = str.split("@");
+ host = sp[1];
+ str = sp[0];
+ }
+
+ if(str.contains("!")) {
+ var sp = str.split("!");
+ user = sp[1];
+ str = sp[0];
+ }
+
+ nick = str;
+
+ return new IrcHostmask(nick, user, host);
+ }
+
+ public String getNick() {
+ return nick;
+ }
+
+ public String getUser() {
+ return user;
+ }
+
+ public String getHost() {
+ return host;
+ }
+}
diff --git a/com/danitheskunk/skunkworks/net/IrcMessage.java b/com/danitheskunk/skunkworks/net/IrcMessage.java
new file mode 100644
index 0000000..bf9118a
--- /dev/null
+++ b/com/danitheskunk/skunkworks/net/IrcMessage.java
@@ -0,0 +1,107 @@
+package com.danitheskunk.skunkworks.net;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class IrcMessage {
+ private String command;
+ private List params;
+ private String prefix;
+
+ public IrcMessage() {
+ params = new ArrayList<>();
+ }
+
+ public IrcMessage(String cmd) {
+ this();
+ command = cmd;
+ }
+
+ public static IrcMessage fromString(String str) {
+ var irc = new IrcMessage();
+
+ if(str.startsWith(":")) {
+ var sp = str.split(" ", 2);
+ irc.setPrefix(sp[0].substring(1));
+ str = sp[1];
+ }
+
+ if(str.contains(" ")) { //has parameters
+ var sp = str.split(" ", 2);
+ irc.setCommand(sp[0]);
+ str = sp[1];
+ while(true) {
+ if(str.startsWith(":")) { //last parameter with :
+ irc.addParam(str.substring(1).strip());
+ break;
+ }
+ if(str.contains(" ")) { //not last parameter
+ sp = str.split(" ", 2);
+ irc.addParam(sp[0]);
+ str = sp[1];
+ } else { //last parameter
+ irc.addParam(str.strip());
+ break;
+ }
+ }
+ } else {
+ irc.setCommand(str.strip());
+ }
+
+ return irc;
+ }
+
+ public IrcMessage addParam(String param) {
+ params.add(param);
+ return this;
+ }
+
+ public String getCommand() {
+ return command;
+ }
+
+ public List getParams() {
+ return params;
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public IrcMessage setCommand(String command) {
+ this.command = command;
+ return this;
+ }
+
+ public IrcMessage setPrefix(String prefix) {
+ this.prefix = prefix;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ var sb = new StringBuilder();
+ if(prefix != null) {
+ sb.append(":");
+ sb.append(prefix);
+ sb.append(" ");
+ }
+ sb.append(command);
+ if(params.size() > 0) {
+ sb.append(" ");
+ }
+ for(int i = 0; i < params.size(); ++i) {
+ if(i == params.size() - 1) {
+ if(params.get(i).contains(" ")) {
+ sb.append(":");
+ }
+ sb.append(params.get(i));
+ } else {
+ sb.append(params.get(i));
+ sb.append(" ");
+ }
+ }
+ sb.append("\r\n");
+ return sb.toString();
+ }
+}
diff --git a/com/danitheskunk/skunkworks/net/Twitch.java b/com/danitheskunk/skunkworks/net/Twitch.java
new file mode 100644
index 0000000..3526fa6
--- /dev/null
+++ b/com/danitheskunk/skunkworks/net/Twitch.java
@@ -0,0 +1,34 @@
+package com.danitheskunk.skunkworks.net;
+
+public class Twitch {
+ private String clientID;
+ private String token;
+ private String username;
+
+ public Twitch(String clientID) {
+ token = TwitchOauth.getToken(clientID,
+ new String[]{"chat:read", "chat:edit"}
+ );
+ this.clientID = clientID;
+ username = getUser();
+ System.out.printf("Logged into twitch as \"%s\"\n", username);
+ }
+
+ public TwitchChat connectChat() {
+ return new TwitchChat(username, token);
+ }
+
+ private String getUser() {
+ var response = HttpUtils.request("https://api.twitch.tv/helix/users").header("Authorization",
+ "Bearer " + token
+ ).header(
+ "Client-Id",
+ clientID
+ ).runJSON();
+ var data = (Object[]) response.get("data");
+ var user = (java.util.Map) data[0];
+ var login = (String) user.get("login");
+
+ return login;
+ }
+}
diff --git a/com/danitheskunk/skunkworks/net/TwitchChat.java b/com/danitheskunk/skunkworks/net/TwitchChat.java
new file mode 100644
index 0000000..9a94671
--- /dev/null
+++ b/com/danitheskunk/skunkworks/net/TwitchChat.java
@@ -0,0 +1,7 @@
+package com.danitheskunk.skunkworks.net;
+
+public class TwitchChat extends IrcClient {
+ public TwitchChat(String nick, String token) {
+ super("irc.chat.twitch.tv", 6667, nick, "oauth:"+token);
+ }
+}
diff --git a/com/danitheskunk/skunkworks/net/TwitchOauth.java b/com/danitheskunk/skunkworks/net/TwitchOauth.java
index 2438862..24f8321 100644
--- a/com/danitheskunk/skunkworks/net/TwitchOauth.java
+++ b/com/danitheskunk/skunkworks/net/TwitchOauth.java
@@ -27,7 +27,7 @@ public class TwitchOauth implements HttpHandler {
private String result = null;
public static String getToken(String clientID, String[] scopes) {
- var scopeStr = String.join("&",
+ var scopeStr = String.join("+",
Arrays.stream(scopes).map((String s) -> URLEncoder.encode(s)).toList()
);
var uri = URI.create(String.format(
@@ -64,6 +64,9 @@ public class TwitchOauth implements HttpHandler {
}
System.out.println("waiting for oauth response");
}
+ if(instance.errored) {
+ throw new RuntimeException("oauth error of some sorts...");
+ }
server.stop(0);
return instance.result;
}
diff --git a/lib/json-io-4.14.0.jar b/lib/json-io-4.14.0.jar
new file mode 100644
index 0000000..0d5b9d2
Binary files /dev/null and b/lib/json-io-4.14.0.jar differ