From e90a6846af051fc452d77aaa4fa18c0e231f7cf0 Mon Sep 17 00:00:00 2001 From: Marco Descher Date: Tue, 8 Oct 2024 07:55:39 +0200 Subject: [PATCH 1/7] Provide Client#resolveRoomAliasSync #12 --- .../jojii/matrixclientserver/Bot/Client.java | 23 ++++++++++ .../Networking/HttpHelper.java | 1 + .../matrixclientserver/Bot/ClientTest.java | 45 ++++++++++++++++--- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/jojii/matrixclientserver/Bot/Client.java b/src/main/java/de/jojii/matrixclientserver/Bot/Client.java index 87a80ee..967da5e 100644 --- a/src/main/java/de/jojii/matrixclientserver/Bot/Client.java +++ b/src/main/java/de/jojii/matrixclientserver/Bot/Client.java @@ -164,6 +164,29 @@ public void leaveRoom(String roomID, EmptyCallback onGone) throws IOException { }); } + /** + * Requests that the server resolve a room alias to a room ID. + * + * @param roomID + * @return the resolved room id or null if no room_id value was + * found + * @throws IOException + * @see https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv3directoryroomroomalias + */ + public String resolveRoomAliasSync(String roomID) throws IOException { + if (roomID.startsWith("#")) { + String response = httpHelper.sendRequest(host, HttpHelper.URLs.directory + "room/" + roomID, null, true, + "GET"); + JSONObject object = new JSONObject(response); + if (object.has("room_id")) { + return object.getString("room_id"); + } else { + return null; + } + } + return roomID; + } + public void sendText(String roomID, String message, DataCallback response) throws IOException { sendText(roomID, message, false, "", response); } diff --git a/src/main/java/de/jojii/matrixclientserver/Networking/HttpHelper.java b/src/main/java/de/jojii/matrixclientserver/Networking/HttpHelper.java index 43e3c42..acf93ba 100644 --- a/src/main/java/de/jojii/matrixclientserver/Networking/HttpHelper.java +++ b/src/main/java/de/jojii/matrixclientserver/Networking/HttpHelper.java @@ -15,6 +15,7 @@ public static class URLs{ public static String client = root+"client/r0/"; public static String media = root+"media/r0/"; + public static String directory = client + "directory/"; public static String login = client+"login"; public static String logout = client+"logout"; public static String logout_all = client+"logout/all"; diff --git a/src/test/java/de/jojii/matrixclientserver/Bot/ClientTest.java b/src/test/java/de/jojii/matrixclientserver/Bot/ClientTest.java index 59916f6..ad3d8f6 100644 --- a/src/test/java/de/jojii/matrixclientserver/Bot/ClientTest.java +++ b/src/test/java/de/jojii/matrixclientserver/Bot/ClientTest.java @@ -1,20 +1,31 @@ package de.jojii.matrixclientserver.Bot; -import de.jojii.matrixclientserver.Callbacks.DataCallback; -import de.jojii.matrixclientserver.Callbacks.EmptyCallback; -import de.jojii.matrixclientserver.Networking.HttpHelper; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import java.io.IOException; + +import org.json.JSONArray; +import org.json.JSONObject; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.internal.matchers.Any; -import java.io.IOException; - -import static org.mockito.Mockito.*; +import de.jojii.matrixclientserver.Callbacks.DataCallback; +import de.jojii.matrixclientserver.Callbacks.EmptyCallback; +import de.jojii.matrixclientserver.Networking.HttpHelper; class ClientTest { - private final HttpHelper httpHelper = Mockito.mock(HttpHelper.class); + private final HttpHelper httpHelper = Mockito.mock(HttpHelper.class, withSettings().verboseLogging()); @Test void joinRoom_notLoggedIn() throws IOException { @@ -87,4 +98,24 @@ void leaveRoom() throws IOException { verify(onGone, times(1)).onRun(); } + + @Test + void resolveRoomAliasSync() throws IOException { + + final String expectedURL = "_matrix/client/r0/directory/room"; + + final Client client = new Client(httpHelper, true); + + JSONObject jsonResponse = new JSONObject(); + jsonResponse.put("room_id", "!hCBUUsLgnlXIvwmnkT:starship-enterprise.com"); + JSONArray servers = new JSONArray(); + servers.put("starship-enterprise.com"); + jsonResponse.put("servers", servers); + + when(httpHelper.sendRequest(isNull(), startsWith(expectedURL), isNull(), eq(true), eq("GET"))) + .thenReturn(jsonResponse.toString()); + + String resolvedId = client.resolveRoomAliasSync("#holodeck:starship-enterprise.com"); + assertEquals("!hCBUUsLgnlXIvwmnkT:starship-enterprise.com", resolvedId); + } } \ No newline at end of file From 98ef8dc6392306f41757ac14600606fef4160ac8 Mon Sep 17 00:00:00 2001 From: Marco Descher Date: Wed, 9 Oct 2024 13:41:07 +0200 Subject: [PATCH 2/7] Multiple fixes and updates --- README.md | 13 ++ .../jojii/matrixclientserver/Bot/Client.java | 217 ++++++++++++++---- .../jojii/matrixclientserver/Bot/Helper.java | 21 ++ .../Networking/HttpHelper.java | 118 ++++++---- .../matrixclientserver/Bot/ClientTest.java | 160 ++++++++----- 5 files changed, 380 insertions(+), 149 deletions(-) diff --git a/README.md b/README.md index a8348c1..4ead3c5 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,22 @@ + # Matrix-ClientServer-API-java A small and simple java API for the Matrix ClientServer Protocol (see [clientServer api](https://matrix.org/docs/spec/client_server/latest)) The API is still in Beta and known for bugs. If you found or missing a feature one you can create a new issue. +Fork of https://github.com/JojiiOfficial/Matrix-ClientServer-API-java with multiple adaptations + +* Add Client#getOrCreateDirectChatRoomSync +* Add Client#getDirectChatRoomsMapSync +* Add Client#resolveRoomAliasSync +* Add Client#loginWithJWTSync +* HttpHelper do not store token, fetch it via Supplier +* Do not copy info (e.g. derive `isLoggedin` via `loginData`) +* HttpHelper pass Authorization via HTTP header not as query parameter +* ... + + ## Usage ### Login diff --git a/src/main/java/de/jojii/matrixclientserver/Bot/Client.java b/src/main/java/de/jojii/matrixclientserver/Bot/Client.java index 967da5e..8564895 100644 --- a/src/main/java/de/jojii/matrixclientserver/Bot/Client.java +++ b/src/main/java/de/jojii/matrixclientserver/Bot/Client.java @@ -1,24 +1,33 @@ package de.jojii.matrixclientserver.Bot; -import org.jetbrains.annotations.Nullable; -import de.jojii.matrixclientserver.Bot.Events.RoomEvent; -import de.jojii.matrixclientserver.Callbacks.*; -import de.jojii.matrixclientserver.Networking.HttpHelper; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - import java.io.IOException; import java.io.InputStream; import java.net.URLEncoder; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import de.jojii.matrixclientserver.Bot.Events.RoomEvent; +import de.jojii.matrixclientserver.Callbacks.DataCallback; +import de.jojii.matrixclientserver.Callbacks.EmptyCallback; +import de.jojii.matrixclientserver.Callbacks.LoginCallback; +import de.jojii.matrixclientserver.Callbacks.MemberCallback; +import de.jojii.matrixclientserver.Callbacks.RoomEventCallback; +import de.jojii.matrixclientserver.Callbacks.RoomEventsCallback; +import de.jojii.matrixclientserver.Networking.HttpHelper; public class Client { private String host; private LoginData loginData; - private boolean isLoggedIn = false; private final HttpHelper httpHelper; private Syncee syncee; @@ -28,33 +37,42 @@ public void login(String username, String password, LoginCallback onResponse) th object.put("user", username); object.put("password", password); httpHelper.sendRequestAsync(host, HttpHelper.URLs.login, object, data -> { - JSONObject object1 = new JSONObject((String) data); - LoginData loginData = new LoginData(); - if (object1.has("response") && object1.getString("response").equals("error") && object1.has("code")) { - loginData.setSuccess(false); - } else { - loginData.setSuccess(true); - isLoggedIn = true; - } - if (loginData.isSuccess()) { - loginData.setAccess_token(object1.getString("access_token")); - loginData.setDevice_id(object1.getString("device_id")); - loginData.setHome_server(object1.getString("home_server")); - loginData.setUser_id(object1.getString("user_id")); - this.loginData = loginData; - httpHelper.setAccess_token(loginData.getAccess_token()); - syncee.startSyncee(); - } + LoginData loginData = Helper.ofPasswordLoginResponse((String) data); + if (loginData.isSuccess()) { + this.loginData = loginData; + syncee.startSyncee(); + } if (onResponse != null) { onResponse.onResponse(loginData); } }); } + public void loginSync(String username, String password) throws IOException { + JSONObject object = new JSONObject(); + object.put("type", "m.login.password"); + object.put("user", username); + object.put("password", password); + String loginResponse = httpHelper.sendRequest(host, HttpHelper.URLs.login, object, false, "POST"); + LoginData loginData = Helper.ofPasswordLoginResponse(loginResponse); + if (loginData.isSuccess()) { + this.loginData = loginData; + syncee.startSyncee(); + } + } + + /** + * Login using a matrix access + * token. + * + * @param userToken + * @param onResponse + * @throws IOException + */ public void login(String userToken, LoginCallback onResponse) throws IOException { - httpHelper.setAccess_token(userToken); httpHelper.sendRequestAsync(host, HttpHelper.URLs.whoami, null, "GET", data -> { - this.isLoggedIn = false; + this.loginData = null; JSONObject object = new JSONObject((String) data); LoginData loginData = new LoginData(); @@ -63,7 +81,6 @@ public void login(String userToken, LoginCallback onResponse) throws IOException loginData.setHome_server(host); loginData.setAccess_token(userToken); loginData.setSuccess(true); - isLoggedIn = true; this.loginData = loginData; syncee.startSyncee(); } else { @@ -73,7 +90,38 @@ public void login(String userToken, LoginCallback onResponse) throws IOException onResponse.onResponse(loginData); } }); + } + /** + * Perform a synchronous login using a JWT token. The matrix server has to + * support this authentication method, else it will fail. + * + * @param jwtToken + * @param deviceId this token is allocated to + * @throws IOException on technical error, or + * @see https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#jwt_config + */ + public void loginWithJWTSync(String jwtToken, @Nullable String deviceId) throws IOException { + JSONObject object = new JSONObject(); + object.put("type", "org.matrix.login.jwt"); + object.put("token", jwtToken); + if (deviceId != null) { + object.put("device_id", deviceId); + } + String loginResponse = httpHelper.sendRequest(host, HttpHelper.URLs.login, object, false, "POST", true); + JSONObject _loginResponse = new JSONObject(loginResponse); + LoginData loginData = new LoginData(); + if (_loginResponse.has("user_id")) { + loginData.setUser_id(_loginResponse.getString("user_id")); + loginData.setHome_server(_loginResponse.getString("home_server")); + loginData.setAccess_token(_loginResponse.getString("access_token")); + loginData.setDevice_id(_loginResponse.getString("device_id")); + loginData.setSuccess(true); + this.loginData = loginData; +// syncee.startSyncee(); + } else { + loginData.setSuccess(false); + } } public void registerRoomEventListener(RoomEventsCallback event) { @@ -85,11 +133,11 @@ public void removeRoomEventListener(RoomEventsCallback event) { } public void logout(EmptyCallback onLoggedOut) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; httpHelper.sendRequestAsync(host, HttpHelper.URLs.logout, null, data -> { - this.isLoggedIn = false; + this.loginData = null; if (onLoggedOut != null) { onLoggedOut.onRun(); } @@ -97,11 +145,11 @@ public void logout(EmptyCallback onLoggedOut) throws IOException { } public void logoutAll(EmptyCallback onLoggedOut) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; httpHelper.sendRequestAsync(host, HttpHelper.URLs.logout_all, null, data -> { - this.isLoggedIn = false; + this.loginData = null; if (onLoggedOut != null) { onLoggedOut.onRun(); } @@ -109,7 +157,7 @@ public void logoutAll(EmptyCallback onLoggedOut) throws IOException { } public void whoami(DataCallback iam) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; httpHelper.sendRequestAsync(host, HttpHelper.URLs.whoami, null, "GET", data -> { @@ -128,7 +176,7 @@ public void setPresence(String presence, String msg, EmptyCallback onStateChange public void setPresence(String userid, String presence, String msg, EmptyCallback onStateChanged) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; JSONObject jsonObject = new JSONObject(); @@ -143,7 +191,7 @@ public void setPresence(String userid, String presence, String msg, EmptyCallbac } public void joinRoom(String roomID, DataCallback onJoined) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; httpHelper.sendRequestAsync(host, HttpHelper.URLs.rooms + roomID + "/join", null, "POST", data -> { @@ -154,7 +202,7 @@ public void joinRoom(String roomID, DataCallback onJoined) throws IOException { } public void leaveRoom(String roomID, EmptyCallback onGone) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; httpHelper.sendRequestAsync(host, HttpHelper.URLs.rooms + roomID + "/leave", null, "POST", data -> { @@ -164,6 +212,68 @@ public void leaveRoom(String roomID, EmptyCallback onGone) throws IOException { }); } + /** + * Get or create a direct chat room with a user. If created the room will be + * registered in m.direct account data, as required in the specification. + * + * @param userID the user id to create the direct chat room with + * @return null if not logged in otherwise the direct chat room id + * @throws IOException + */ + public String getOrCreateDirectChatRoomSync(String userID) throws IOException { + if (!isLoggedIn()) { + return null; + } + + Map> directChatRooms = getDirectChatRoomsMapSync(); + List list = directChatRooms.get(userID); + if (list != null && !list.isEmpty()) { + return list.get(0); + } + + // create the direct chat room + JSONObject jsonObject = new JSONObject(); + jsonObject.put("is_direct", Boolean.TRUE); + jsonObject.put("preset", "trusted_private_chat"); + jsonObject.put("invite", new JSONArray().put(userID)); + + String response = httpHelper.sendRequest(host, HttpHelper.URLs.client + "createRoom", jsonObject, true, "POST", + true); + JSONObject object = new JSONObject(response); + String roomID = object.getString("room_id"); + + // register in users account_data m.direct + directChatRooms.put(userID, Collections.singletonList(roomID)); + JSONObject mDirect = new JSONObject(directChatRooms); + httpHelper.sendRequest(host, HttpHelper.URLs.user + loginData.getUser_id() + "/account_data/m.direct", mDirect, + true, "PUT"); + + return roomID; + } + + /** + * Returns the map of the registered user's direct chat rooms. + * + * @return null if not logged in, otherwise a map containing the + * users id as keys and the respective direct chat room ids. + * @throws IOException + * @see https://spec.matrix.org/v1.11/client-server-api/#mdirect + */ + public Map> getDirectChatRoomsMapSync() throws IOException { + if (loginData == null) { + return null; + } + String response = httpHelper.sendRequest(host, + HttpHelper.URLs.user + loginData.getUser_id() + "/account_data/m.direct", null, true, "GET"); + JSONObject jsonObject = new JSONObject(response); + return jsonObject.keySet().stream() + .collect(Collectors.toMap(key -> (String) key, + key -> IntStream.range(0, jsonObject.getJSONArray((String) key).length()) + .mapToObj(i -> jsonObject.getJSONArray((String) key).getString(i)) + .collect(Collectors.toList()))); + } + /** * Requests that the server resolve a room alias to a room ID. * @@ -192,7 +302,7 @@ public void sendText(String roomID, String message, DataCallback response) throw } public void sendText(String roomID, String message, boolean formatted, String formattedMessage, DataCallback response) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; JSONObject data = new JSONObject(); @@ -207,7 +317,7 @@ public void sendText(String roomID, String message, boolean formatted, String fo } public void sendMessage(String roomID, JSONObject messageObject, DataCallback response) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; sendRoomEvent("m.room.message", roomID, messageObject, response); @@ -223,7 +333,7 @@ public void sendRoomEvent(String event, String roomID, JSONObject content, DataC } public void kickUser(String roomID, String userID, String reason, DataCallback response) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; JSONObject ob = new JSONObject(); @@ -238,7 +348,7 @@ public void kickUser(String roomID, String userID, String reason, DataCallback r } public void banUser(String roomID, String userID, String reason, DataCallback response) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; JSONObject ob = new JSONObject(); @@ -253,7 +363,7 @@ public void banUser(String roomID, String userID, String reason, DataCallback re } public void unbanUser(String roomID, String userID, DataCallback response) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; JSONObject ob = new JSONObject(); @@ -268,7 +378,7 @@ public void unbanUser(String roomID, String userID, DataCallback response) throw public void sendReadReceipt(String roomID, String eventID, String receiptType, DataCallback response) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; httpHelper.sendRequestAsync(host, HttpHelper.URLs.rooms + roomID + "/receipt/" + receiptType + "/" + eventID, null, "POST", data -> { @@ -283,7 +393,7 @@ public void setTyping(boolean typing, String roomID, int timeout, EmptyCallback } public void setTyping(boolean typing, String userid, String roomID, int timeout, EmptyCallback response) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; JSONObject object = new JSONObject(); @@ -298,7 +408,7 @@ public void setTyping(boolean typing, String userid, String roomID, int timeout, } public void getRoomMembers(String roomID, MemberCallback memberCallback) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; httpHelper.sendRequestAsync(host, HttpHelper.URLs.rooms + roomID + "/joined_members", null, "GET", data -> { @@ -330,7 +440,7 @@ public void getRoomMembers(String roomID, MemberCallback memberCallback) throws } public void getRoomEventFromId(String roomID, String eventID, RoomEventCallback callback) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; httpHelper.sendRequestAsync(host, HttpHelper.URLs.rooms + URLEncoder.encode(roomID) + "/event/" + URLEncoder.encode(eventID), null, "GET", data -> { @@ -348,7 +458,7 @@ public void getRoomEventFromId(String roomID, String eventID, RoomEventCallback public void createRoom(String preset, String visibility, @Nullable String alias, String name, @Nullable String topic, @Nullable List invitations, @Nullable String roomVersion, DataCallback callback) throws IOException { - if (!isLoggedIn) + if (!isLoggedIn()) return; JSONObject object = new JSONObject(); @@ -417,7 +527,9 @@ public static class Room { public Client(String host) { this.host = host; - this.httpHelper = new HttpHelper(); + this.httpHelper = new HttpHelper(() -> { + return loginData != null ? loginData.getAccess_token() : null; + }); this.syncee = new Syncee(this, httpHelper); if (!host.endsWith("/")) this.host += "/"; @@ -429,7 +541,12 @@ public Client(String host) { public Client(HttpHelper httpHelper, boolean isLoggedIn) { this.httpHelper = httpHelper; this.syncee = new Syncee(this, httpHelper); - this.isLoggedIn = isLoggedIn; + if (isLoggedIn) { + LoginData loginData = new LoginData(); + loginData.setSuccess(true); + loginData.setUser_id("@data:starship-enterprise.com"); + this.loginData = loginData; + } } public String getHost() { @@ -441,6 +558,6 @@ public LoginData getLoginData() { } public boolean isLoggedIn() { - return isLoggedIn; + return loginData != null ? loginData.isSuccess() : false; } } \ No newline at end of file diff --git a/src/main/java/de/jojii/matrixclientserver/Bot/Helper.java b/src/main/java/de/jojii/matrixclientserver/Bot/Helper.java index f532cb5..a7a0676 100644 --- a/src/main/java/de/jojii/matrixclientserver/Bot/Helper.java +++ b/src/main/java/de/jojii/matrixclientserver/Bot/Helper.java @@ -2,9 +2,30 @@ import java.util.Random; +import org.json.JSONObject; + public class Helper { public static int randomInt(int min, int max){ Random r = new Random(); return r.nextInt((max - min) + 1) + min; } + + public static LoginData ofPasswordLoginResponse(String loginResponse) { + JSONObject _loginResponse = new JSONObject(loginResponse); + LoginData loginData = new LoginData(); + if (_loginResponse.has("response") && _loginResponse.getString("response").equals("error") + && _loginResponse.has("code")) { + loginData.setSuccess(false); + } else { + loginData.setSuccess(true); + } + if (loginData.isSuccess()) { + loginData.setAccess_token(_loginResponse.getString("access_token")); + loginData.setDevice_id(_loginResponse.getString("device_id")); + loginData.setHome_server(_loginResponse.getString("home_server")); + loginData.setUser_id(_loginResponse.getString("user_id")); + } + return loginData; + } + } diff --git a/src/main/java/de/jojii/matrixclientserver/Networking/HttpHelper.java b/src/main/java/de/jojii/matrixclientserver/Networking/HttpHelper.java index acf93ba..4f6a623 100644 --- a/src/main/java/de/jojii/matrixclientserver/Networking/HttpHelper.java +++ b/src/main/java/de/jojii/matrixclientserver/Networking/HttpHelper.java @@ -5,9 +5,11 @@ import java.io.*; import java.net.HttpURLConnection; +import java.net.URI; import java.net.URL; import java.net.URLConnection; import java.nio.charset.StandardCharsets; +import java.util.function.Supplier; public class HttpHelper { public static class URLs{ @@ -26,56 +28,76 @@ public static class URLs{ public static String user = client+"user/"; public static String upload = media+"upload/"; } - private String access_token; - public void setAccess_token(String access_token) { - this.access_token = access_token; - } - - public String sendRequest(String host, String path, JSONObject data, boolean useAccesstoken, String requestMethod) throws IOException { - String surl = host+path + (useAccesstoken ? "?access_token="+access_token : ""); - /*if(!path.contains("sync")){ - System.out.println(surl); - }*/ - URL obj = new URL(surl); - URLConnection con = obj.openConnection(); - HttpURLConnection http = (HttpURLConnection)con; - http.setRequestMethod(requestMethod); - http.setDoOutput(true); - http.setReadTimeout(60000); + public HttpHelper(Supplier accessTokenSupplier) { + this.accessTokenSupplier = accessTokenSupplier; + } - if(data != null){ - int length = data.toString().length(); - http.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); - } - - http.connect(); + private Supplier accessTokenSupplier; - if(data != null){ - try(OutputStream os = http.getOutputStream()) { - os.write(data.toString().getBytes()); - } - } - - try(BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))) { - StringBuilder response = new StringBuilder(); - String responseLine = null; - while ((responseLine = br.readLine()) != null) { - response.append(responseLine.trim()); - } - return response.toString(); - }catch (IOException e){ - return "{\n" + - " \"response\":\"error\",\n" + - " \"code\":"+http.getResponseCode()+"\n" + - "}"; - } + public String sendRequest(String host, String path, JSONObject data, boolean useAccesstoken, String requestMethod) throws IOException { + return sendRequest(host, path, data, useAccesstoken, requestMethod, false); + } + + public String sendRequest(String host, String path, JSONObject data, boolean useAccesstoken, String requestMethod, + boolean throwAll) throws IOException { + + URL obj = URI.create(host + path).toURL(); + URLConnection con = obj.openConnection(); + + HttpURLConnection http = (HttpURLConnection) con; + http.setRequestMethod(requestMethod); + http.setDoOutput(true); + http.setReadTimeout(60000); + + if (useAccesstoken) { + http.setRequestProperty("Authorization", "Bearer " + accessTokenSupplier.get()); + } + + if (data != null) { + int length = data.toString().length(); + http.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); + } + + http.connect(); + + if (data != null) { + try (OutputStream os = http.getOutputStream()) { + os.write(data.toString().getBytes()); + } + } + + try (BufferedReader br = new BufferedReader( + new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))) { + StringBuilder response = new StringBuilder(); + String responseLine = null; + while ((responseLine = br.readLine()) != null) { + response.append(responseLine.trim()); + } + return response.toString(); + } catch (IOException e) { + + String errorResponse = null; + InputStream errorStream = http.getErrorStream(); + if (errorStream != null) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(errorStream))) { + StringBuilder response = new StringBuilder(); + String responseLine = null; + while ((responseLine = br.readLine()) != null) { + response.append(responseLine.trim()); + } + errorResponse = response.toString(); + } + } + if (throwAll) { + throw new IOException(errorResponse, e); + } + return "{\n" + " \"response\":\"error\",\n" + " \"code\":" + http.getResponseCode() + "\n" + "}"; + } } public String sendStream(String host, String path, String contentType, InputStream data, int contentLength, boolean useAccesstoken, String requestMethod) throws IOException { - String surl = host+path + (useAccesstoken ? "?access_token="+access_token : ""); - - URL obj = new URL(surl); + URL obj = URI.create(host + path).toURL(); URLConnection con = obj.openConnection(); HttpURLConnection http = (HttpURLConnection)con; http.setRequestMethod(requestMethod); @@ -83,6 +105,10 @@ public String sendStream(String host, String path, String contentType, InputStre http.setRequestProperty("Content-Type", contentType); http.addRequestProperty("Content-Length", Integer.toString(contentLength)); + if (useAccesstoken) { + http.setRequestProperty("Authorization", "Bearer " + accessTokenSupplier.get()); + } + try(OutputStream os = http.getOutputStream()) { int i = 0; int bytes = 0; @@ -130,11 +156,11 @@ public void sendStreamAsync(String host, String path, String contentType, int co } public void sendRequestAsync(String host, String path, JSONObject data, DataCallback callback) throws IOException { - sendRequestAsync(host, path, data, callback, access_token != null, "POST"); + sendRequestAsync(host, path, data, callback, accessTokenSupplier.get() != null, "POST"); } public void sendRequestAsync(String host, String path, JSONObject data, String requestMethd, DataCallback callback) throws IOException { - sendRequestAsync(host, path, data, callback, access_token != null, requestMethd); + sendRequestAsync(host, path, data, callback, accessTokenSupplier.get() != null, requestMethd); } public void sendRequestAsync(String host, String path, JSONObject data, DataCallback callback, boolean useAccesstoken, String requestMethod) throws IOException { diff --git a/src/test/java/de/jojii/matrixclientserver/Bot/ClientTest.java b/src/test/java/de/jojii/matrixclientserver/Bot/ClientTest.java index ad3d8f6..0a909c6 100644 --- a/src/test/java/de/jojii/matrixclientserver/Bot/ClientTest.java +++ b/src/test/java/de/jojii/matrixclientserver/Bot/ClientTest.java @@ -1,7 +1,9 @@ package de.jojii.matrixclientserver.Bot; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNotNull; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.startsWith; import static org.mockito.Mockito.never; @@ -11,6 +13,9 @@ import static org.mockito.Mockito.withSettings; import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; import org.json.JSONArray; import org.json.JSONObject; @@ -27,77 +32,73 @@ class ClientTest { private final HttpHelper httpHelper = Mockito.mock(HttpHelper.class, withSettings().verboseLogging()); - @Test - void joinRoom_notLoggedIn() throws IOException { - Client client = new Client(httpHelper, false); - DataCallback callback = Mockito.mock(DataCallback.class); + @Test + void joinRoom_notLoggedIn() throws IOException { + Client client = new Client(httpHelper, false); + DataCallback callback = Mockito.mock(DataCallback.class); - client.joinRoom("myRoom", callback); + client.joinRoom("myRoom", callback); - verify(callback, never()).onData(Any.ANY); - } + verify(callback, never()).onData(Any.ANY); + } - @Test - void joinRoom() throws IOException { - final String expectedURL = "_matrix/client/r0/rooms/myRoom/join"; + @Test + void joinRoom() throws IOException { + final String expectedURL = "_matrix/client/r0/rooms/myRoom/join"; - final Client client = new Client(httpHelper, true); - final DataCallback onJoined = Mockito.mock(DataCallback.class); + final Client client = new Client(httpHelper, true); + final DataCallback onJoined = Mockito.mock(DataCallback.class); - // invoke method - client.joinRoom("myRoom", onJoined); + // invoke method + client.joinRoom("myRoom", onJoined); - final ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(DataCallback.class); - verify(httpHelper) - .sendRequestAsync(isNull(), eq(expectedURL), isNull(), eq("POST"), callbackCaptor.capture()); - callbackCaptor.getValue().onData("test"); + final ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(DataCallback.class); + verify(httpHelper).sendRequestAsync(isNull(), eq(expectedURL), isNull(), eq("POST"), callbackCaptor.capture()); + callbackCaptor.getValue().onData("test"); - verify(onJoined, times(1)).onData("test"); - } + verify(onJoined, times(1)).onData("test"); + } - @Test - void joinRoom_callbackIsNull() throws IOException { - final String expectedURL = "_matrix/client/r0/rooms/myRoom/join"; + @Test + void joinRoom_callbackIsNull() throws IOException { + final String expectedURL = "_matrix/client/r0/rooms/myRoom/join"; - final Client client = new Client(httpHelper, true); - client.joinRoom("myRoom", null); + final Client client = new Client(httpHelper, true); + client.joinRoom("myRoom", null); - final ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(DataCallback.class); - verify(httpHelper) - .sendRequestAsync(isNull(), eq(expectedURL), isNull(), eq("POST"), callbackCaptor.capture()); - callbackCaptor.getValue().onData("test"); - } + final ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(DataCallback.class); + verify(httpHelper).sendRequestAsync(isNull(), eq(expectedURL), isNull(), eq("POST"), callbackCaptor.capture()); + callbackCaptor.getValue().onData("test"); + } - @Test - void leaveRoom_callbackIsNull() throws IOException { - final String expectedURL = "_matrix/client/r0/rooms/myRoom/leave"; + @Test + void leaveRoom_callbackIsNull() throws IOException { + final String expectedURL = "_matrix/client/r0/rooms/myRoom/leave"; - final Client client = new Client(httpHelper, true); - client.leaveRoom("myRoom", null); + final Client client = new Client(httpHelper, true); + client.leaveRoom("myRoom", null); - final ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(DataCallback.class); - verify(httpHelper) - .sendRequestAsync(isNull(), eq(expectedURL), isNull(), eq("POST"), callbackCaptor.capture()); - callbackCaptor.getValue().onData("test"); - } + final ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(DataCallback.class); + verify(httpHelper).sendRequestAsync(isNull(), eq(expectedURL), isNull(), eq("POST"), callbackCaptor.capture()); + callbackCaptor.getValue().onData("test"); + } - @Test - void leaveRoom() throws IOException { - final String expectedURL = "_matrix/client/r0/rooms/familyChat/leave"; + @Test + void leaveRoom() throws IOException { + final String expectedURL = "_matrix/client/r0/rooms/familyChat/leave"; - final Client client = new Client(httpHelper, true); - final EmptyCallback onGone = Mockito.mock(EmptyCallback.class); + final Client client = new Client(httpHelper, true); + final EmptyCallback onGone = Mockito.mock(EmptyCallback.class); - // invoke method - client.leaveRoom("familyChat", onGone); + // invoke method + client.leaveRoom("familyChat", onGone); - final ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(DataCallback.class); - verify(httpHelper) - .sendRequestAsync(isNull(), eq(expectedURL), isNull(), eq("POST"), callbackCaptor.capture()); - callbackCaptor.getValue().onData("test"); + final ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(DataCallback.class); + verify(httpHelper).sendRequestAsync(isNull(), eq(expectedURL), isNull(), eq("POST"), callbackCaptor.capture()); + callbackCaptor.getValue().onData("test"); - verify(onGone, times(1)).onRun(); - } + verify(onGone, times(1)).onRun(); + } @Test void resolveRoomAliasSync() throws IOException { @@ -118,4 +119,57 @@ void resolveRoomAliasSync() throws IOException { String resolvedId = client.resolveRoomAliasSync("#holodeck:starship-enterprise.com"); assertEquals("!hCBUUsLgnlXIvwmnkT:starship-enterprise.com", resolvedId); } + + @Test + void getDirectRoomMapSync() throws IOException { + final Client client = new Client(httpHelper, true); + performLogin(client); + + final String expectedURL = "_matrix/client/r0/user/@data:starship-enterprise.com/account_data/m.direct"; + + JSONObject jsonResponse = new JSONObject(); + JSONArray jsonArray = new JSONArray(); + jsonArray.put("!rGkalNKwzOdTviJzbY:starship-enterprise.com"); + jsonResponse.put("@picard:starship-enterprise.com", jsonArray); + + when(httpHelper.sendRequest(isNull(), startsWith(expectedURL), isNull(), eq(true), eq("GET"))) + .thenReturn(jsonResponse.toString()); + + Map> directRoomMap = client.getDirectChatRoomsMapSync(); + List list = directRoomMap.get("@picard:starship-enterprise.com"); + assertEquals("!rGkalNKwzOdTviJzbY:starship-enterprise.com", list.get(0)); + } + + @Test + void getOrCreateDirectChatRoomSync() throws IOException { + final Client client = new Client(httpHelper, true); + performLogin(client); + + // no direct message room for @geordi found, will need to create + String expectedURL = "_matrix/client/r0/user/@data:starship-enterprise.com/account_data/m.direct"; + when(httpHelper.sendRequest(isNull(), startsWith(expectedURL), isNull(), eq(true), eq("GET"))) + .thenReturn(new JSONObject().toString()); + // TODO do not deliver empty but other, then test if correct update + + expectedURL = "_matrix/client/r0/createRoom"; + when(httpHelper.sendRequest(isNull(), startsWith(expectedURL), isNotNull(), eq(true), eq("POST"), eq(true))) + .thenReturn("{\"room_id\":\"!geordihere:starship-enterprise.com\"}"); + + String roomId = client.getOrCreateDirectChatRoomSync("@geordi:starship-enterprise.com"); + assertEquals("!geordihere:starship-enterprise.com", roomId); + } + + private void performLogin(Client client) throws IOException { + JSONObject loginResponse = new JSONObject(); + loginResponse.put("user_id", "@data:starship-enterprise.com"); + loginResponse.put("access_token", "syt_c2VydmljZS1hY2NvdW50LWVsZXhpcy1zZXJ2ZXI_ZwTAYorZbOeMzLGRBDTt_3ltbrk"); + loginResponse.put("home_server", "starship-enterprise.com"); + loginResponse.put("device_id", "mockito"); + + when(httpHelper.sendRequest(isNull(), eq(HttpHelper.URLs.login), isNotNull(), eq(false), eq("POST"))) + .thenReturn(loginResponse.toString()); + client.loginSync("@data:starship-enterprise.com", "doesnotcompute"); + assertTrue(client.isLoggedIn()); + } + } \ No newline at end of file From 5dcb9e57b0de052a2b2f1c04240d9843d2633061 Mon Sep 17 00:00:00 2001 From: Marco Descher Date: Tue, 15 Oct 2024 12:37:02 +0200 Subject: [PATCH 3/7] Create maven.yml --- .github/workflows/maven.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/maven.yml diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..1a6afc6 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,35 @@ +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Java CI with Maven + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml + + # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive + - name: Update dependency graph + uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 From 09ac4d705adcc12ae10bc9176e7c8f16ece0ec0e Mon Sep 17 00:00:00 2001 From: Marco Descher Date: Tue, 15 Oct 2024 12:43:04 +0200 Subject: [PATCH 4/7] Update maven.yml --- .github/workflows/maven.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 1a6afc6..73323f1 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -30,6 +30,39 @@ jobs: - name: Build with Maven run: mvn -B package --file pom.xml + # Upload the build artifacts + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: build-artifacts + path: target/*.jar + # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: Update dependency graph uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 + + release: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: target/*.jar + asset_name: build-artifacts.jar + asset_content_type: application/java-archive From 8e9c22e2c157e74332af6c574dc22e4306db0f87 Mon Sep 17 00:00:00 2001 From: Marco Descher Date: Tue, 15 Oct 2024 12:45:58 +0200 Subject: [PATCH 5/7] Update maven.yml --- .github/workflows/maven.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 73323f1..57a1ac3 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -45,6 +45,9 @@ jobs: needs: build runs-on: ubuntu-latest steps: + - name: Extract tag name + id: extract_tag + run: echo "::set-output name=tag::${GITHUB_REF#refs/tags/}" - uses: actions/checkout@v4 - name: Create Release id: create_release @@ -52,8 +55,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} + tag_name: ${{ steps.extract_tag.outputs.tag }} + release_name: Release ${{ steps.extract_tag.outputs.tag }} draft: false prerelease: false From aefd20dbf2072a59c9201467be91d0fec84072aa Mon Sep 17 00:00:00 2001 From: Marco Descher Date: Tue, 15 Oct 2024 12:48:45 +0200 Subject: [PATCH 6/7] Update maven.yml --- .github/workflows/maven.yml | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 57a1ac3..c77d6c3 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -40,32 +40,3 @@ jobs: # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: Update dependency graph uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 - - release: - needs: build - runs-on: ubuntu-latest - steps: - - name: Extract tag name - id: extract_tag - run: echo "::set-output name=tag::${GITHUB_REF#refs/tags/}" - - uses: actions/checkout@v4 - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.extract_tag.outputs.tag }} - release_name: Release ${{ steps.extract_tag.outputs.tag }} - draft: false - prerelease: false - - - name: Upload Release Asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: target/*.jar - asset_name: build-artifacts.jar - asset_content_type: application/java-archive From fbafc62a6ace3ea5e8e10d03c8b99669f635e849 Mon Sep 17 00:00:00 2001 From: Marco Descher Date: Tue, 15 Oct 2024 12:56:46 +0200 Subject: [PATCH 7/7] Create maven-publish.yml --- .github/workflows/maven-publish.yml | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/maven-publish.yml diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml new file mode 100644 index 0000000..5f5d24a --- /dev/null +++ b/.github/workflows/maven-publish.yml @@ -0,0 +1,34 @@ +# This workflow will build a package using Maven and then publish it to GitHub packages when a release is created +# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path + +name: Maven Package + +on: + release: + types: [created] + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + server-id: github # Value of the distributionManagement/repository/id field of the pom.xml + settings-path: ${{ github.workspace }} # location for the settings.xml file + + - name: Build with Maven + run: mvn -B package --file pom.xml + + - name: Publish to GitHub Packages Apache Maven + run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml + env: + GITHUB_TOKEN: ${{ github.token }}