diff --git a/projects/catculator/README.md b/projects/catculator/README.md new file mode 100644 index 0000000..782091b --- /dev/null +++ b/projects/catculator/README.md @@ -0,0 +1,8 @@ +# Catculator + +Native implementations of some of Kit Tunes features. + +## Development + +Test new binaries by setting the `catculator.natives_path` system property when running the game: +Example: `-Dcatculator.natives_path=/home/lilly/projects/kit-tunes/projects/catculator/target/debug` diff --git a/projects/catculator/src/main/java/net/pixaurora/catculator/api/error/ClientResponseException.java b/projects/catculator/src/main/java/net/pixaurora/catculator/api/error/ClientResponseException.java new file mode 100644 index 0000000..521c25e --- /dev/null +++ b/projects/catculator/src/main/java/net/pixaurora/catculator/api/error/ClientResponseException.java @@ -0,0 +1,7 @@ +package net.pixaurora.catculator.api.error; + +public class ClientResponseException extends Exception { + public ClientResponseException(String message) { + super(message); + } +} diff --git a/projects/catculator/src/main/java/net/pixaurora/catculator/api/http/Client.java b/projects/catculator/src/main/java/net/pixaurora/catculator/api/http/Client.java new file mode 100644 index 0000000..a3275fd --- /dev/null +++ b/projects/catculator/src/main/java/net/pixaurora/catculator/api/http/Client.java @@ -0,0 +1,18 @@ +package net.pixaurora.catculator.api.http; + +import net.pixaurora.catculator.impl.http.ClientImpl; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +public interface Client extends AutoCloseable { + static @NotNull Client create(String userAgent) throws IOException { + return new ClientImpl(userAgent); + } + + @NotNull RequestBuilder get(String url); + @NotNull RequestBuilder post(String url); + + @Override + void close(); // Remove throws Exception from AutoCloseable +} diff --git a/projects/catculator/src/main/java/net/pixaurora/catculator/api/http/RequestBuilder.java b/projects/catculator/src/main/java/net/pixaurora/catculator/api/http/RequestBuilder.java new file mode 100644 index 0000000..fc70421 --- /dev/null +++ b/projects/catculator/src/main/java/net/pixaurora/catculator/api/http/RequestBuilder.java @@ -0,0 +1,12 @@ +package net.pixaurora.catculator.api.http; + +import net.pixaurora.catculator.api.error.ClientResponseException; +import org.jetbrains.annotations.NotNull; + +public interface RequestBuilder { + @NotNull Response send() throws ClientResponseException; + + @NotNull RequestBuilder body(byte[] data); + @NotNull RequestBuilder query(String key, String value); + @NotNull RequestBuilder header(String key, String value); +} diff --git a/projects/catculator/src/main/java/net/pixaurora/catculator/api/http/Response.java b/projects/catculator/src/main/java/net/pixaurora/catculator/api/http/Response.java new file mode 100644 index 0000000..50eebac --- /dev/null +++ b/projects/catculator/src/main/java/net/pixaurora/catculator/api/http/Response.java @@ -0,0 +1,14 @@ +package net.pixaurora.catculator.api.http; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface Response { + int status(); + byte[] body(); + @Nullable String header(@NotNull String name); + + default boolean ok() { + return this.status() >= 200 && this.status() < 300; + } +} diff --git a/projects/catculator/src/main/java/net/pixaurora/catculator/api/http/Server.java b/projects/catculator/src/main/java/net/pixaurora/catculator/api/http/Server.java index 98080a7..5612f90 100644 --- a/projects/catculator/src/main/java/net/pixaurora/catculator/api/http/Server.java +++ b/projects/catculator/src/main/java/net/pixaurora/catculator/api/http/Server.java @@ -10,7 +10,7 @@ public static Server create() { return new ServerImpl(); } - public String runServer() throws IOException; + public String run() throws IOException; public void close(); } diff --git a/projects/catculator/src/main/java/net/pixaurora/catculator/impl/http/ClientImpl.java b/projects/catculator/src/main/java/net/pixaurora/catculator/impl/http/ClientImpl.java new file mode 100644 index 0000000..2397a21 --- /dev/null +++ b/projects/catculator/src/main/java/net/pixaurora/catculator/impl/http/ClientImpl.java @@ -0,0 +1,49 @@ +package net.pixaurora.catculator.impl.http; + +import net.pixaurora.catculator.api.http.Client; +import net.pixaurora.catculator.api.http.RequestBuilder; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +public class ClientImpl implements Client { + private final long ptr; + private boolean active = true; + + public ClientImpl(String userAgent) throws IOException { + this.ptr = create(userAgent); + } + + @Override + public @NotNull RequestBuilder get(String url) { + if (this.active) { + return this.request("GET", url); + } else { + throw new RuntimeException("HTTP client inactive."); + } + } + + @Override + public @NotNull RequestBuilder post(String url) { + if (this.active) { + return this.request("POST", url); + } else { + throw new RuntimeException("HTTP client inactive."); + } + } + + @Override + public void close() { + if (!this.active) { + return; + } + + this.drop(); + this.active = false; + } + + private native @NotNull RequestBuilder request(String method, String url); + + private static native long create(@NotNull String userAgent) throws IOException; + private native void drop(); +} diff --git a/projects/catculator/src/main/java/net/pixaurora/catculator/impl/http/RequestBuilderImpl.java b/projects/catculator/src/main/java/net/pixaurora/catculator/impl/http/RequestBuilderImpl.java new file mode 100644 index 0000000..18e2efa --- /dev/null +++ b/projects/catculator/src/main/java/net/pixaurora/catculator/impl/http/RequestBuilderImpl.java @@ -0,0 +1,42 @@ +package net.pixaurora.catculator.impl.http; + +import net.pixaurora.catculator.api.http.RequestBuilder; +import net.pixaurora.catculator.api.http.Response; +import org.jetbrains.annotations.NotNull; + +public class RequestBuilderImpl implements RequestBuilder { + private long ptr; + + private RequestBuilderImpl(long ptr) { + this.ptr = ptr; + } + + @Override + public @NotNull Response send() { + return this.send0(); + } + + @Override + public @NotNull RequestBuilder body(byte[] data) { + this.body0(data); + return this; + } + + @Override + public @NotNull RequestBuilder query(String key, String value) { + this.query0(key, value); + return this; + } + + @Override + public @NotNull RequestBuilder header(String key, String value) { + this.header0(key, value); + return this; + } + + private native @NotNull Response send0(); + + private native void body0(byte[] data); + private native void query0(String key, String value); + private native void header0(String key, String value); +} diff --git a/projects/catculator/src/main/java/net/pixaurora/catculator/impl/http/ResponseImpl.java b/projects/catculator/src/main/java/net/pixaurora/catculator/impl/http/ResponseImpl.java new file mode 100644 index 0000000..397833d --- /dev/null +++ b/projects/catculator/src/main/java/net/pixaurora/catculator/impl/http/ResponseImpl.java @@ -0,0 +1,34 @@ +package net.pixaurora.catculator.impl.http; + +import net.pixaurora.catculator.api.http.Response; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +public class ResponseImpl implements Response { + private final int status; + private final byte[] body; + private final Map headers; + + private ResponseImpl(int status, byte[] body, Map headers) { + this.status = status; + this.body = body; + this.headers = headers; + } + + @Override + public int status() { + return this.status; + } + + @Override + public byte[] body() { + return this.body; + } + + @Override + public @Nullable String header(@NotNull String name) { + return this.headers.get(name); + } +} diff --git a/projects/catculator/src/main/java/net/pixaurora/catculator/impl/http/ServerImpl.java b/projects/catculator/src/main/java/net/pixaurora/catculator/impl/http/ServerImpl.java index b7c8e3b..f660432 100644 --- a/projects/catculator/src/main/java/net/pixaurora/catculator/impl/http/ServerImpl.java +++ b/projects/catculator/src/main/java/net/pixaurora/catculator/impl/http/ServerImpl.java @@ -3,20 +3,20 @@ import net.pixaurora.catculator.api.http.Server; public class ServerImpl implements Server { - private final long pointer; + private final long ptr; public ServerImpl() { - this.pointer = create(); + this.ptr = create(); } private static native long create(); @Override - public String runServer() { - return this.runServer0(); + public String run() { + return this.run0(); } - private native String runServer0(); + private native String run0(); @Override public void close() { diff --git a/projects/catculator/src/main/java/net/pixaurora/catculator/impl/util/JniUtil.java b/projects/catculator/src/main/java/net/pixaurora/catculator/impl/util/JniUtil.java new file mode 100644 index 0000000..bd87c40 --- /dev/null +++ b/projects/catculator/src/main/java/net/pixaurora/catculator/impl/util/JniUtil.java @@ -0,0 +1,12 @@ +package net.pixaurora.catculator.impl.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +public class JniUtil { + private static @NotNull Map newMap() { + return new HashMap<>(); + } +} diff --git a/projects/catculator/src/main/rust/bridge.rs b/projects/catculator/src/main/rust/bridge.rs deleted file mode 100644 index ef241e0..0000000 --- a/projects/catculator/src/main/rust/bridge.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::Result; -use jni::{objects::JObject, sys::jlong, JNIEnv}; - -mod http; -mod sound_parsing; - -fn get_pointer<'local>(env: &mut JNIEnv<'local>, object: &JObject<'local>) -> Result { - Ok(env.get_field(object, "pointer", "J")?.try_into()?) -} diff --git a/projects/catculator/src/main/rust/bridge/http.rs b/projects/catculator/src/main/rust/bridge/http.rs deleted file mode 100644 index df4adf9..0000000 --- a/projects/catculator/src/main/rust/bridge/http.rs +++ /dev/null @@ -1 +0,0 @@ -mod server; diff --git a/projects/catculator/src/main/rust/bridge/http/client.rs b/projects/catculator/src/main/rust/bridge/http/client.rs new file mode 100644 index 0000000..bad39fe --- /dev/null +++ b/projects/catculator/src/main/rust/bridge/http/client.rs @@ -0,0 +1,83 @@ +use jni::{ + objects::{JClass, JObject, JString, JValue}, + sys::jlong, + JNIEnv, +}; +use reqwest::{blocking::Client, Method}; + +use crate::{ + bridge::{drop_box, pack_box, pull_box}, + utils::JStringToString, + Error, Result, +}; + +const REQUEST_BUILDER_CLASS: &str = "net/pixaurora/catculator/impl/http/RequestBuilderImpl"; + +fn create(env: &mut JNIEnv, user_agent: JString) -> Result { + let client = Client::builder() + .https_only(true) + .user_agent(user_agent.to_string(env)?) + .build()?; + + Ok(pack_box(client)) +} + +#[no_mangle] +pub extern "system" fn Java_net_pixaurora_catculator_impl_http_ClientImpl_create<'r>( + mut env: JNIEnv<'r>, + _class: JClass<'r>, + user_agent: JString<'r>, +) -> jlong { + match create(&mut env, user_agent) { + Ok(ptr) => return ptr, + Err(error) => error.throw(&mut env), + } + + jlong::default() +} + +#[no_mangle] +pub extern "system" fn Java_net_pixaurora_catculator_impl_http_ClientImpl_drop<'r>( + mut env: JNIEnv<'r>, + this: JObject<'r>, +) -> () { + if let Err(error) = drop_box::(&mut env, &this) { + panic!("Couldn't drop http client due to an error! {}", error); + } +} + +fn request<'r>( + env: &mut JNIEnv<'r>, + this: &JObject<'r>, + method: JString<'r>, + url: JString<'r>, +) -> Result> { + let client = pull_box::(env, this)?; + + let method = match Method::from_bytes(method.to_string(env)?.as_bytes()) { + Ok(method) => method, + Err(_) => return Err(Error::String(String::from("Invalid HTTP method."))), + }; + + let ptr = pack_box(client.request(method, url.to_string(env)?)); + + let class = env.find_class(REQUEST_BUILDER_CLASS)?; + let instance = env.new_object(class, "(J)V", &[JValue::Long(ptr)])?; + + Ok(instance) +} + +#[no_mangle] +pub extern "system" fn Java_net_pixaurora_catculator_impl_http_ClientImpl_request<'r>( + mut env: JNIEnv<'r>, + this: JObject<'r>, + method: JString<'r>, + url: JString<'r>, +) -> JObject<'r> { + match request(&mut env, &this, method, url) { + Ok(object) => return object, + Err(error) => error.throw(&mut env), + }; + + JObject::null() +} diff --git a/projects/catculator/src/main/rust/bridge/http/request_builder.rs b/projects/catculator/src/main/rust/bridge/http/request_builder.rs new file mode 100644 index 0000000..32622cd --- /dev/null +++ b/projects/catculator/src/main/rust/bridge/http/request_builder.rs @@ -0,0 +1,161 @@ +use jni::{ + objects::{JByteArray, JObject, JString, JValue}, + sys::jint, + JNIEnv, +}; +use reqwest::{blocking::RequestBuilder, header::HeaderValue}; + +use crate::{ + bridge::{haul_box, new_map, pack_box_into}, + utils::JStringToString, + Result, +}; + +const RESPONSE_CLASS: &str = "net/pixaurora/catculator/impl/http/ResponseImpl"; + +fn send<'r>(env: &mut JNIEnv<'r>, this: &JObject<'r>) -> Result> { + let builder = haul_box::(env, this)?; + + let (client, request) = builder.build_split(); + let mut request = request?; + + // For some reason Reqwest doesn't set this by default + if !request.method().is_safe() && !request.headers().contains_key("Content-Length") { + request + .headers_mut() + .append("Content-Length", HeaderValue::from_str("0").unwrap()); + } + + let response = client.execute(request)?; + + let status: jint = response.status().as_u16().into(); + + let map = new_map(env)?; + let map_ops = env.get_map(&map)?; + + for (name, value) in response.headers().iter() { + let name = env.new_string(name.as_str())?; + let value = env.new_string(value.to_str()?)?; + + map_ops.put(env, &name, &value)?; + } + + let data = response.bytes()?.to_vec(); + let body = env.new_byte_array(data.len() as i32)?; + + let buffer: Vec = unsafe { std::mem::transmute(data) }; + env.set_byte_array_region(&body, 0, &buffer[..])?; + + let class = env.find_class(RESPONSE_CLASS)?; + let instance = env.new_object( + class, + "(I[BLjava/util/Map;)V", + &[ + JValue::Int(status), + JValue::Object(&body), + JValue::Object(&map), + ], + )?; + + Ok(instance) +} + +#[no_mangle] +pub extern "system" fn Java_net_pixaurora_catculator_impl_http_RequestBuilderImpl_send0<'r>( + mut env: JNIEnv<'r>, + this: JObject<'r>, +) -> JObject<'r> { + match send(&mut env, &this) { + Ok(response) => return response, + Err(error) => error.throw(&mut env), + }; + + JObject::null() +} + +fn body<'r>(env: &mut JNIEnv<'r>, this: &JObject<'r>, data: JByteArray<'r>) -> Result<()> { + let request_builder = haul_box::(env, this)?; + + let length = env.get_array_length(&data)? as usize; + let mut buffer = vec![0; length]; + + env.get_byte_array_region(data, 0, &mut buffer[..])?; + let buffer: Vec = unsafe { std::mem::transmute(buffer) }; + + pack_box_into( + env, + this, + request_builder + .body(buffer) + .header("Content-Length", length), + )?; + + Ok(()) +} + +#[no_mangle] +pub extern "system" fn Java_net_pixaurora_catculator_impl_http_RequestBuilderImpl_body0<'r>( + mut env: JNIEnv<'r>, + this: JObject<'r>, + data: JByteArray<'r>, +) -> () { + if let Err(error) = body(&mut env, &this, data) { + error.throw(&mut env); + } +} + +fn query<'r>( + env: &mut JNIEnv<'r>, + this: &JObject<'r>, + key: JString<'r>, + value: JString<'r>, +) -> Result<()> { + let request_builder = haul_box::(env, this)?; + + let key = key.to_string(env)?; + let value = value.to_string(env)?; + + pack_box_into(env, this, request_builder.query(&[(key, value)]))?; + + Ok(()) +} + +#[no_mangle] +pub extern "system" fn Java_net_pixaurora_catculator_impl_http_RequestBuilderImpl_query0<'r>( + mut env: JNIEnv<'r>, + this: JObject<'r>, + key: JString<'r>, + value: JString<'r>, +) -> () { + if let Err(error) = query(&mut env, &this, key, value) { + error.throw(&mut env); + } +} + +fn header<'r>( + env: &mut JNIEnv<'r>, + this: &JObject<'r>, + key: JString<'r>, + value: JString<'r>, +) -> Result<()> { + let request_builder = haul_box::(env, this)?; + + let key = key.to_string(env)?; + let value = value.to_string(env)?; + + pack_box_into(env, this, request_builder.header(key, value))?; + + Ok(()) +} + +#[no_mangle] +pub extern "system" fn Java_net_pixaurora_catculator_impl_http_RequestBuilderImpl_header0<'r>( + mut env: JNIEnv<'r>, + this: JObject<'r>, + key: JString<'r>, + value: JString<'r>, +) -> () { + if let Err(error) = header(&mut env, &this, key, value) { + error.throw(&mut env); + } +} diff --git a/projects/catculator/src/main/rust/bridge/http/server.rs b/projects/catculator/src/main/rust/bridge/http/server.rs index 2480c46..5ced2b5 100644 --- a/projects/catculator/src/main/rust/bridge/http/server.rs +++ b/projects/catculator/src/main/rust/bridge/http/server.rs @@ -1,4 +1,7 @@ -use crate::Result; +use crate::{ + bridge::{drop_box, pack_box, pull_box}, + Result, +}; use jni::{ objects::{JClass, JObject, JString}, @@ -7,20 +10,10 @@ use jni::{ }; use tokio::runtime::Runtime; -use crate::{bridge::get_pointer, http::server::Server}; - -fn create<'local>() -> jlong { - let server = Server::new(); - - Box::into_raw(Box::from(server)) as jlong -} +use crate::http::server::Server; -fn run_server<'local>( - object: &JObject<'local>, - env: &mut JNIEnv<'local>, -) -> Result> { - let pointer = get_pointer(env, object)?; - let server = unsafe { &mut *(pointer as *mut Server) }; +fn run_server<'r>(object: &JObject<'r>, env: &mut JNIEnv<'r>) -> Result> { + let server = pull_box::(env, object)?; let runtime = Runtime::new()?; let token = runtime.block_on(server.run_server(&runtime))?; @@ -28,42 +21,33 @@ fn run_server<'local>( Ok(env.new_string(token)?) } -fn drop<'local>(object: &JObject<'local>, env: &mut JNIEnv<'local>) -> Result<()> { - let pointer = get_pointer(env, object)?; - #[allow(unused_variables)] - let server = unsafe { Box::from_raw(pointer as *mut Server) }; - - Ok(()) -} - #[no_mangle] -pub extern "system" fn Java_net_pixaurora_catculator_impl_http_ServerImpl_create<'local>( - mut _env: JNIEnv<'local>, - _class: JClass<'local>, +pub extern "system" fn Java_net_pixaurora_catculator_impl_http_ServerImpl_create<'r>( + mut _env: JNIEnv<'r>, + _class: JClass<'r>, ) -> jlong { - create() + pack_box(Server::new()) } #[no_mangle] -pub extern "system" fn Java_net_pixaurora_catculator_impl_http_ServerImpl_runServer0<'local>( - mut env: JNIEnv<'local>, - object: JObject<'local>, -) -> JString<'local> { +pub extern "system" fn Java_net_pixaurora_catculator_impl_http_ServerImpl_run0<'r>( + mut env: JNIEnv<'r>, + object: JObject<'r>, +) -> JString<'r> { match run_server(&object, &mut env) { - Ok(token) => token, - Err(error) => { - error.throw(&mut env); - JString::default().into() - } + Ok(token) => return token, + Err(error) => error.throw(&mut env), } + + JString::default() } #[no_mangle] -pub extern "system" fn Java_net_pixaurora_catculator_impl_http_ServerImpl_drop<'local>( - mut env: JNIEnv<'local>, - object: JObject<'local>, +pub extern "system" fn Java_net_pixaurora_catculator_impl_http_ServerImpl_drop<'r>( + mut env: JNIEnv<'r>, + object: JObject<'r>, ) -> () { - if let Err(error) = drop(&object, &mut env) { - panic!("Couldn't drop server due to an error! {}", error); + if let Err(error) = drop_box::(&mut env, &object) { + panic!("Couldn't drop http server due to an error! {}", error); } } diff --git a/projects/catculator/src/main/rust/bridge/mod.rs b/projects/catculator/src/main/rust/bridge/mod.rs new file mode 100644 index 0000000..e4dbd5a --- /dev/null +++ b/projects/catculator/src/main/rust/bridge/mod.rs @@ -0,0 +1,65 @@ +use crate::Result; +use jni::{ + objects::{JObject, JValue}, + sys::jlong, + JNIEnv, +}; + +mod http { + mod client; + mod request_builder; + mod server; +} +mod sound_parsing; + +const UTIL_CLASS: &str = "net/pixaurora/catculator/impl/util/JniUtil"; + +fn pack_box(t: T) -> jlong { + Box::into_raw(Box::from(t)) as jlong +} + +fn pack_box_into(env: &mut JNIEnv, object: &JObject, t: T) -> Result<()> { + let pointer = pack_box(t); + env.set_field(object, "ptr", "J", JValue::Long(pointer))?; + + Ok(()) +} + +/** +Receive ownership of the boxed item owned by the Java object. +*/ +fn haul_box(env: &mut JNIEnv, object: &JObject) -> Result { + let pointer = get_pointer(env, object)?; + + #[allow(unused_variables)] + let value = unsafe { Box::from_raw(pointer as *mut T) }; + + Ok(*value) +} + +fn drop_box(env: &mut JNIEnv, object: &JObject) -> Result<()> { + let pointer = get_pointer(env, object)?; + + #[allow(unused_variables)] + let value = unsafe { Box::from_raw(pointer as *mut T) }; + + Ok(()) +} + +fn pull_box<'r, T>(env: &mut JNIEnv, object: &JObject) -> Result<&'r mut T> { + let pointer = get_pointer(env, object)?; + Ok(unsafe { &mut *(pointer as *mut T) }) +} + +fn get_pointer(env: &mut JNIEnv, object: &JObject) -> Result { + Ok(env.get_field(object, "ptr", "J")?.j()?) +} + +fn new_map<'r>(env: &mut JNIEnv<'r>) -> Result> { + let class = env.find_class(UTIL_CLASS)?; + let object = env + .call_static_method(class, "newMap", "()Ljava/util/Map;", &[])? + .l()?; + + Ok(object) +} diff --git a/projects/catculator/src/main/rust/bridge/sound_parsing.rs b/projects/catculator/src/main/rust/bridge/sound_parsing.rs index 45b3e69..119d548 100644 --- a/projects/catculator/src/main/rust/bridge/sound_parsing.rs +++ b/projects/catculator/src/main/rust/bridge/sound_parsing.rs @@ -7,7 +7,7 @@ use jni::{ use crate::sound_parsing::audio_length; use crate::Result; -fn parse_duration<'local>(env: &mut JNIEnv<'local>, path: &JString<'local>) -> Result { +fn parse_duration<'r>(env: &mut JNIEnv<'r>, path: &JString<'r>) -> Result { let path: String = env.get_string(path)?.into(); let duration = audio_length(path)?; @@ -16,16 +16,15 @@ fn parse_duration<'local>(env: &mut JNIEnv<'local>, path: &JString<'local>) -> R } #[no_mangle] -pub extern "system" fn Java_net_pixaurora_catculator_api_music_SoundFile_parseDuration0<'local>( - mut env: JNIEnv<'local>, - _class: JClass<'local>, - path: JString<'local>, +pub extern "system" fn Java_net_pixaurora_catculator_api_music_SoundFile_parseDuration0<'r>( + mut env: JNIEnv<'r>, + _class: JClass<'r>, + path: JString<'r>, ) -> jlong { match parse_duration(&mut env, &path) { - Ok(duration) => duration, - Err(error) => { - error.throw(&mut env); - jlong::default() - } + Ok(duration) => return duration, + Err(error) => error.throw(&mut env), } + + jlong::default() } diff --git a/projects/catculator/src/main/rust/errors.rs b/projects/catculator/src/main/rust/errors.rs index 4505061..93310f5 100644 --- a/projects/catculator/src/main/rust/errors.rs +++ b/projects/catculator/src/main/rust/errors.rs @@ -1,9 +1,11 @@ use std::fmt::Display; use std::num::TryFromIntError; +use std::str::Utf8Error; use jni::errors::Error as JNIError; use jni::JNIEnv; use lofty::error::LoftyError; +use reqwest::header::ToStrError; use rocket::Error as RocketError; use std::io::Error as IOError; use tokio::sync::mpsc::error::TryRecvError; @@ -13,10 +15,13 @@ use tokio::task::JoinError; pub enum Error { JNI(JNIError), IO(IOError), + Utf8Error(Utf8Error), Rocket(RocketError), Recv(TryRecvError), + Reqwest(reqwest::Error), Join(JoinError), - Server(String), + String(String), + ToStrError(ToStrError), Lofty(LoftyError), TryFromInt(TryFromIntError), } @@ -38,10 +43,13 @@ impl Display for Error { match self { Error::JNI(error) => error.fmt(f), Error::IO(error) => error.fmt(f), + Error::Utf8Error(error) => error.fmt(f), Error::Rocket(error) => error.fmt(f), Error::Recv(error) => error.fmt(f), + Error::Reqwest(error) => error.fmt(f), Error::Join(error) => error.fmt(f), - Error::Server(message) => write!(f, "Server Error: {}", message), + Error::String(message) => write!(f, "Server Error: {}", message), + Error::ToStrError(error) => error.fmt(f), Error::Lofty(error) => error.fmt(f), Error::TryFromInt(error) => error.fmt(f), } @@ -60,21 +68,39 @@ impl From for Error { } } +impl From for Error { + fn from(value: Utf8Error) -> Self { + Error::Utf8Error(value) + } +} + impl From for Error { fn from(value: RocketError) -> Self { Error::Rocket(value) } } +impl From for Error { + fn from(value: TryRecvError) -> Self { + Error::Recv(value) + } +} + +impl From for Error { + fn from(value: reqwest::Error) -> Self { + Error::Reqwest(value) + } +} + impl From for Error { fn from(value: JoinError) -> Self { Error::Join(value) } } -impl From for Error { - fn from(value: TryRecvError) -> Self { - Error::Recv(value) +impl From for Error { + fn from(value: ToStrError) -> Self { + Error::ToStrError(value) } } diff --git a/projects/catculator/src/main/rust/http.rs b/projects/catculator/src/main/rust/http.rs deleted file mode 100644 index 74f47ad..0000000 --- a/projects/catculator/src/main/rust/http.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod server; diff --git a/projects/catculator/src/main/rust/http/server.rs b/projects/catculator/src/main/rust/http/server.rs index 7fc7162..8179220 100644 --- a/projects/catculator/src/main/rust/http/server.rs +++ b/projects/catculator/src/main/rust/http/server.rs @@ -67,7 +67,7 @@ impl Server { match token { Some(token) => Ok(token), - None => Err(Error::Server("Token was not obtained.".into())), + None => Err(Error::String("Token was not obtained.".into())), } } diff --git a/projects/catculator/src/main/rust/lib.rs b/projects/catculator/src/main/rust/lib.rs index 40d3ad1..fa06103 100644 --- a/projects/catculator/src/main/rust/lib.rs +++ b/projects/catculator/src/main/rust/lib.rs @@ -1,8 +1,11 @@ mod bridge; pub mod errors; -pub mod http; +pub mod http { + pub mod server; +} pub mod sound_parsing; +pub mod utils; pub use errors::Error; pub use errors::Result; diff --git a/projects/catculator/src/main/rust/utils.rs b/projects/catculator/src/main/rust/utils.rs new file mode 100644 index 0000000..028a131 --- /dev/null +++ b/projects/catculator/src/main/rust/utils.rs @@ -0,0 +1,13 @@ +use jni::{objects::JString, JNIEnv}; + +use crate::Result; + +pub trait JStringToString { + fn to_string(&self, env: &mut JNIEnv) -> Result; +} + +impl<'r> JStringToString for JString<'r> { + fn to_string(&self, env: &mut JNIEnv) -> Result { + Ok(env.get_string(&self)?.to_str()?.to_owned()) + } +} diff --git a/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/KitTunes.java b/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/KitTunes.java index d4b941b..7cf0ee5 100644 --- a/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/KitTunes.java +++ b/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/KitTunes.java @@ -1,10 +1,12 @@ package net.pixaurora.kitten_heart.impl; +import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import net.pixaurora.catculator.api.http.Client; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,6 +23,18 @@ import net.pixaurora.catculator.impl.Catculator; public class KitTunes { + static { + Catculator.init(); + + try { + CLIENT = Client.create(buildUserAgent()); + } catch (IOException e) { + throw new RuntimeException("Failed to create HTTP client", e); + } + } + + public static final Client CLIENT; + public static final Logger LOGGER = LoggerFactory.getLogger(Constants.MOD_ID); public static final ConfigManager SCROBBLER_CACHE = new ConfigManager<>( @@ -42,8 +56,6 @@ public static void init() { // doing this can sometimes cause issues. MusicMetadata.init(MusicMetadataLoader.albumFiles(), MusicMetadataLoader.artistFiles(), MusicMetadataLoader.trackFiles()); - - Catculator.init(); } public static void tick() { @@ -51,6 +63,11 @@ public static void tick() { } public static void stop() { + CLIENT.close(); EventHandling.stop(); } + + private static String buildUserAgent() { + return "Kit Tunes/" + Constants.MOD_VERSION + " (+" + Constants.HOMEPAGE + ")"; + } } diff --git a/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/network/HttpHelper.java b/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/network/HttpHelper.java deleted file mode 100644 index f069573..0000000 --- a/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/network/HttpHelper.java +++ /dev/null @@ -1,106 +0,0 @@ -package net.pixaurora.kitten_heart.impl.network; - -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import net.pixaurora.kitten_heart.impl.Constants; -import net.pixaurora.kitten_heart.impl.KitTunes; -import net.pixaurora.kitten_heart.impl.error.UnhandledKitTunesException; - -public class HttpHelper { - public static InputStream get(String endpoint, Map queryParameters) - throws UnhandledKitTunesException { - return UnhandledKitTunesException.runOrThrow(() -> handleRequest("GET", endpoint, queryParameters)); - } - - public static InputStream post(String endpoint, Map queryParameters) - throws UnhandledKitTunesException { - return UnhandledKitTunesException.runOrThrow(() -> handleRequest("POST", endpoint, queryParameters)); - } - - public static Map defaultHeaders() { - Map headers = new HashMap<>(); - - headers.put("User-Agent", "Kit Tunes/" + Constants.MOD_VERSION + " (+" + Constants.HOMEPAGE + ")"); - - return headers; - } - - public static void logResponse(InputStream data) throws IOException { - StringBuilder builder = new StringBuilder(); - - int c = data.read(); - while (c != -1) { - builder.append((char) c); - - c = data.read(); - } - - KitTunes.LOGGER.info("Received response: " + builder.toString()); - } - - private static InputStream handleRequest(String method, String endpoint, Map queryParameters) - throws IOException { - URL url = new URL(endpoint + createQuery(queryParameters)); - - HttpURLConnection connection = narrowConnection(url.openConnection()); - - connection.setRequestMethod(method); - - // Set headers - defaultHeaders().forEach((key, value) -> connection.setRequestProperty(key, value)); - connection.setRequestProperty("Content-Length", "0"); - - if (method == "POST") { // Only if POSTing, set Content-Length - connection.setDoOutput(true); - connection.setFixedLengthStreamingMode(0); - } - - connection.getContentLength(); - - if (connection.getResponseCode() == 200) { - return connection.getInputStream(); - } else { - return connection.getErrorStream(); - } - } - - private static String createQuery(Map queryParameters) { - List query = new ArrayList<>(queryParameters.size()); - - for (Map.Entry parameter : queryParameters.entrySet()) { - try { - query.add(parameter.getKey() + "=" - + URLEncoder.encode(parameter.getValue(), StandardCharsets.UTF_8.toString())); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Should never happen, URL encoding is hard-coded.", e); - } - } - - return query.size() == 0 ? "" : "?" + String.join("&", query); - } - - public static boolean isUnreserved(char value) { - return ('A' <= value && value <= 'Z') || ('a' <= value && value <= 'z') || ('0' <= value && value <= '9') - || value == '-' || value == '_' || value == '.' || value == '~'; - } - - private static HttpURLConnection narrowConnection(URLConnection connection) throws UnhandledKitTunesException { - if (connection instanceof HttpURLConnection) { - return (HttpURLConnection) connection; - } else { - throw new UnhandledKitTunesException( - "URL Connection must be of type HttpURLConnection, not `" + connection.getClass().getName() + "`!"); - } - } -} diff --git a/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/scrobble/LastFMScrobbler.java b/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/scrobble/LastFMScrobbler.java index a28d571..13b9c90 100644 --- a/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/scrobble/LastFMScrobbler.java +++ b/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/scrobble/LastFMScrobbler.java @@ -1,6 +1,8 @@ package net.pixaurora.kitten_heart.impl.scrobble; +import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -8,21 +10,24 @@ import java.util.Optional; import java.util.stream.Collectors; +import net.pixaurora.catculator.api.error.ClientResponseException; +import net.pixaurora.catculator.api.http.RequestBuilder; +import net.pixaurora.catculator.api.http.Response; +import net.pixaurora.kitten_heart.impl.KitTunes; +import net.pixaurora.kitten_heart.impl.error.UnhandledKitTunesException; import org.w3c.dom.Document; import org.w3c.dom.Node; import net.pixaurora.kitten_heart.impl.error.KitTunesException; import net.pixaurora.kitten_heart.impl.error.ScrobblerParsingException; -import net.pixaurora.kitten_heart.impl.error.UnhandledKitTunesException; import net.pixaurora.kitten_heart.impl.network.Encryption; -import net.pixaurora.kitten_heart.impl.network.HttpHelper; import net.pixaurora.kitten_heart.impl.network.XMLHelper; public class LastFMScrobbler implements Scrobbler { public static final String API_KEY = "6f9e533b5f6631a5aa3070f5e757de8c"; public static final String SHARED_SECRET = "97fbf9a3d76ba36dfb5a2f6c3215bf49"; - public static final String ROOT_API_URL = "http://ws.audioscrobbler.com/2.0/"; + public static final String ROOT_API_URL = "https://ws.audioscrobbler.com/2.0/"; public static final String SETUP_URL = "https://last.fm/api/auth?api_key=" + API_KEY; public static final ScrobblerType TYPE = new ScrobblerType<>("last.fm.new", LastFMScrobbler.class, @@ -45,46 +50,61 @@ public String username() { @Override public void startScrobbling(ScrobbleInfo track) throws KitTunesException { - Map args = new HashMap<>(); + Map query = new HashMap<>(); - args.put("method", "track.updateNowPlaying"); + query.put("method", "track.updateNowPlaying"); - args.put("artist", track.artistTitle()); - args.put("track", track.trackTitle()); - args.put("api_key", this.apiKey()); - args.put("sk", this.session.key); + query.put("artist", track.artistTitle()); + query.put("track", track.trackTitle()); + query.put("api_key", this.apiKey()); + query.put("sk", this.session.key); if (track.albumTitle().isPresent()) { - args.put("album", track.albumTitle().get()); + query.put("album", track.albumTitle().get()); } - this.handleScrobbling(this.addSignature(args)); + this.submitScrobble(this.addSignature(query)); } @Override public void completeScrobbling(ScrobbleInfo track) throws KitTunesException { - Map args = new HashMap<>(); + Map query = new HashMap<>(); - args.put("method", "track.scrobble"); + query.put("method", "track.scrobble"); - args.put("artist", track.artistTitle()); - args.put("track", track.trackTitle()); - args.put("timestamp", String.valueOf(track.startTime().getEpochSecond())); - args.put("api_key", this.apiKey()); - args.put("sk", this.session.key); + query.put("artist", track.artistTitle()); + query.put("track", track.trackTitle()); + query.put("timestamp", String.valueOf(track.startTime().getEpochSecond())); + query.put("api_key", this.apiKey()); + query.put("sk", this.session.key); Optional albumTitle = track.albumTitle(); if (albumTitle.isPresent()) { - args.put("album", albumTitle.get()); + query.put("album", albumTitle.get()); } - this.handleScrobbling(this.addSignature(args)); + this.submitScrobble(this.addSignature(query)); } - private void handleScrobbling(Map args) throws KitTunesException { - InputStream responseBody = HttpHelper.post(ROOT_API_URL, args); + private void submitScrobble(Map query) throws KitTunesException { + RequestBuilder builder = KitTunes.CLIENT.post(ROOT_API_URL); + + for (Map.Entry entry : query.entrySet()) { + builder.query(entry.getKey(), entry.getValue()); + } + + Response response = null; + + try { + response = builder.send(); + } catch (ClientResponseException e) { + KitTunes.LOGGER.error("Failed to submit scrobble.", e); + } - UnhandledKitTunesException.runOrThrow(() -> HttpHelper.logResponse(responseBody)); + if (response != null) { + String message = new String(response.body(), StandardCharsets.UTF_8); + KitTunes.LOGGER.info("Received {} with body {}.", response.status(), message); + } } private Map addSignature(Map parameters) { @@ -109,19 +129,36 @@ private static Map addSignature(Map parameters, } private static LastFMSession createSession(String token) throws ScrobblerParsingException { - Map args = new HashMap<>(); + Map query = new HashMap<>(); + + query.put("method", "auth.getSession"); + query.put("api_key", API_KEY); + query.put("token", token); - args.put("method", "auth.getSession"); - args.put("api_key", API_KEY); - args.put("token", token); + RequestBuilder builder = KitTunes.CLIENT.get(ROOT_API_URL); - InputStream responseBody = HttpHelper.get(ROOT_API_URL, addSignature(args, SHARED_SECRET)); + for (Map.Entry entry : addSignature(query, SHARED_SECRET).entrySet()) { + builder.query(entry.getKey(), entry.getValue()); + } - Document body = XMLHelper.getDocument(responseBody); + Response response = null; - Node root = XMLHelper.requireChild("lfm", body); + try { + response = builder.send(); + } catch (ClientResponseException e) { + throw new UnhandledKitTunesException(e); + } - return LastFMSession.fromXML("session", root); + if (response == null || !response.ok()) { + throw new UnhandledKitTunesException("Response not ok"); + } else { + InputStream stream = new ByteArrayInputStream(response.body()); + + Document body = XMLHelper.getDocument(stream); + Node root = XMLHelper.requireChild("lfm", body); + + return LastFMSession.fromXML("session", root); + } } @Override diff --git a/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/scrobble/ScrobblerSetup.java b/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/scrobble/ScrobblerSetup.java index c8f0bcf..49de151 100644 --- a/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/scrobble/ScrobblerSetup.java +++ b/projects/kitten-heart/src/main/java/net/pixaurora/kitten_heart/impl/scrobble/ScrobblerSetup.java @@ -22,7 +22,7 @@ public ScrobblerSetup(Server server, ScrobblerType scrobblerType, long timeou private String run() { try { - return this.server.runServer(); + return this.server.run(); } catch (IOException e) { throw new RuntimeException("Couldn't finish running server!", e); }