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