diff --git a/datastore/src/main/java/net/jami/datastore/dao/ConversationDao.java b/datastore/src/main/java/net/jami/datastore/dao/ConversationDao.java new file mode 100644 index 0000000000000000000000000000000000000000..b115d4f567e3d6761c2ffdd4a97ccf2f5d8040d8 --- /dev/null +++ b/datastore/src/main/java/net/jami/datastore/dao/ConversationDao.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2023 by Savoir-faire Linux + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package net.jami.datastore.dao; + +import lombok.extern.slf4j.Slf4j; + +import net.jami.datastore.main.DataStore; +import net.jami.jams.common.dao.connectivity.SQLConnection; +import net.jami.jams.common.objects.conversations.Conversation; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.List; + +@Slf4j +public class ConversationDao extends AbstractDao<Conversation> { + + public ConversationDao() { + this.setTableName("conversations"); + this.setTClass(Conversation.class); + } + + public List<Conversation> getByOwner(String owner) { + ResultSet rs = executeQuery("SELECT * FROM conversations WHERE owner = ?", owner); + return getObjectsFromResultSet(rs); + } + + @Override + public boolean storeObject(Conversation object) { + String query = + "INSERT INTO conversations (owner, id, created, removed, erased, members, lastDisplayed)" + + " VALUES (?, ?, ?, ?, ?, ?, ?)"; + return executeInsert(query, object); + } + + public boolean storeConversationList(List<Conversation> conversationList) { + if (conversationList.isEmpty()) { + log.error("Cannot store empty conversation list"); + return false; + } + + SQLConnection connection = DataStore.connectionPool.getConnection(); + try { + // Initiate transaction + connection.getConnection().setAutoCommit(false); + String update = + "UPDATE conversations SET created = ?, removed = ?, erased = ?, members = ?, lastDisplayed = ?" + + "WHERE owner = ? AND id = ?"; + + String insert = + "INSERT INTO conversations (owner, id, created, removed, erased, members, lastDisplayed) VALUES " + + "(?, ?, ?, ?, ?, ?, ?)"; + for (Conversation conversation : conversationList) { + PreparedStatement updatePs = connection.getConnection().prepareStatement(update); + conversation.getUpdate(updatePs); + int rowsUpdated = updatePs.executeUpdate(); + + if (rowsUpdated == 0) { + // If no rows were updated, perform an insert + PreparedStatement insertPs = + connection.getConnection().prepareStatement(insert); + conversation.getInsert(insertPs); + insertPs.executeUpdate(); + } + } + // Commit transaction + connection.getConnection().commit(); + return true; + } catch (Exception e) { + log.error("Could not update conversations: {}", e.getMessage()); + return false; + } finally { + DataStore.connectionPool.returnConnection(connection); + } + } +} diff --git a/datastore/src/main/java/net/jami/datastore/dao/ConversationRequestDao.java b/datastore/src/main/java/net/jami/datastore/dao/ConversationRequestDao.java new file mode 100644 index 0000000000000000000000000000000000000000..a3f568cce86f795000a3d9c859bbc30be7afc288 --- /dev/null +++ b/datastore/src/main/java/net/jami/datastore/dao/ConversationRequestDao.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2023 by Savoir-faire Linux + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package net.jami.datastore.dao; + +import lombok.extern.slf4j.Slf4j; + +import net.jami.datastore.main.DataStore; +import net.jami.jams.common.dao.connectivity.SQLConnection; +import net.jami.jams.common.objects.conversations.ConversationRequest; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.List; + +@Slf4j +public class ConversationRequestDao extends AbstractDao<ConversationRequest> { + + public ConversationRequestDao() { + this.setTableName("conversations"); + this.setTClass(ConversationRequest.class); + } + + public List<ConversationRequest> getByOwner(String owner) { + ResultSet rs = executeQuery("SELECT * FROM conversation_requests WHERE owner = ?", owner); + return getObjectsFromResultSet(rs); + } + + @Override + public boolean storeObject(ConversationRequest object) { + String query = + "INSERT INTO conversation_requests (owner, conversationId, sender, metadatas, received, declined)" + + " VALUES (?, ?, ?, ?, ?, ?)"; + return executeInsert(query, object); + } + + public boolean storeConversationRequestList(List<ConversationRequest> conversationRequestList) { + if (conversationRequestList.isEmpty()) { + log.error("Cannot store empty conversationRequest list"); + return false; + } + + SQLConnection connection = DataStore.connectionPool.getConnection(); + try { + // Initiate transaction + connection.getConnection().setAutoCommit(false); + String update = + "UPDATE conversation_requests SET sender = ?, metadatas = ?, received = ?, declined = ?" + + "WHERE owner = ? AND conversationId = ?"; + + String insert = + "INSERT INTO conversation_requests (owner, conversationId, sender, metadatas, received, declined) VALUES " + + "(?, ?, ?, ?, ?, ?)"; + for (ConversationRequest conversationRequest : conversationRequestList) { + PreparedStatement updatePs = connection.getConnection().prepareStatement(update); + conversationRequest.getUpdate(updatePs); + int rowsUpdated = updatePs.executeUpdate(); + + if (rowsUpdated == 0) { + // If no rows were updated, perform an insert + PreparedStatement insertPs = + connection.getConnection().prepareStatement(insert); + conversationRequest.getInsert(insertPs); + insertPs.executeUpdate(); + } + } + // Commit transaction + connection.getConnection().commit(); + return true; + } catch (Exception e) { + log.error("Could not update conversationRequests: {}", e.getMessage()); + return false; + } finally { + DataStore.connectionPool.returnConnection(connection); + } + } +} diff --git a/datastore/src/main/java/net/jami/datastore/main/DataStore.java b/datastore/src/main/java/net/jami/datastore/main/DataStore.java index 80e9491e47102073eea3d4a22227900c715ae015..08aba9c4e5b5a695bdd5230464666061f4eaaa97 100644 --- a/datastore/src/main/java/net/jami/datastore/main/DataStore.java +++ b/datastore/src/main/java/net/jami/datastore/main/DataStore.java @@ -26,6 +26,8 @@ import lombok.Getter; import lombok.Setter; import net.jami.datastore.dao.ContactDao; +import net.jami.datastore.dao.ConversationDao; +import net.jami.datastore.dao.ConversationRequestDao; import net.jami.datastore.dao.DeviceDao; import net.jami.datastore.dao.GroupDao; import net.jami.datastore.dao.PolicyDao; @@ -58,6 +60,8 @@ public class DataStore implements AuthenticationSource { private DeviceDao deviceDao; private SystemDao systemDao; private ContactDao contactDao; + private ConversationDao conversationDao; + private ConversationRequestDao conversationRequestDao; private UserProfileDao userProfileDao; private UserGroupMappingsDao userGroupMappingsDao; public static final Integer RESULTS_PER_PAGE = 24; @@ -74,6 +78,8 @@ public class DataStore implements AuthenticationSource { deviceDao = new DeviceDao(); systemDao = new SystemDao(); contactDao = new ContactDao(); + conversationDao = new ConversationDao(); + conversationRequestDao = new ConversationRequestDao(); userProfileDao = new UserProfileDao(); userGroupMappingsDao = new UserGroupMappingsDao(); } diff --git a/datastore/src/main/resources/db/migration/V38__Conversations.sql b/datastore/src/main/resources/db/migration/V38__Conversations.sql new file mode 100644 index 0000000000000000000000000000000000000000..0c55f54a139d68cd4cf1c34921d89041b1aacbd1 --- /dev/null +++ b/datastore/src/main/resources/db/migration/V38__Conversations.sql @@ -0,0 +1,4 @@ +CREATE TABLE conversations (owner varchar(255), +id varchar(255), created bigint, removed bigint, +erased bigint, members clob, lastDisplayed varchar(255), +PRIMARY KEY (owner,id)); \ No newline at end of file diff --git a/datastore/src/main/resources/db/migration/V39__ConversationRequests.sql b/datastore/src/main/resources/db/migration/V39__ConversationRequests.sql new file mode 100644 index 0000000000000000000000000000000000000000..3be4a5520ae32f1a74d548b3b1d278802aab9852 --- /dev/null +++ b/datastore/src/main/resources/db/migration/V39__ConversationRequests.sql @@ -0,0 +1,4 @@ +CREATE TABLE conversation_requests (owner varchar(255), +conversationId varchar(255), sender varchar(255), metadatas clob, +received bigint, declined bigint, +PRIMARY KEY (owner,conversationId)); \ No newline at end of file diff --git a/datastore/src/test/java/net/jami/datastore/dao/ConversationDaoTest.java b/datastore/src/test/java/net/jami/datastore/dao/ConversationDaoTest.java new file mode 100644 index 0000000000000000000000000000000000000000..47724c210359a45d72904e8b4007b7c2ac8f3377 --- /dev/null +++ b/datastore/src/test/java/net/jami/datastore/dao/ConversationDaoTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 by Savoir-faire Linux + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package net.jami.datastore.dao; + +import net.jami.datastore.main.DataStore; +import net.jami.jams.common.dao.connectivity.SQLConnection; +import net.jami.jams.common.objects.conversations.Conversation; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +class ConversationDaoTest { + private ConversationDao conversationDao; + + @BeforeEach + void setUp() { + DataStore dataStore = new DataStore("jdbc:derby:memory:testdb;create=true"); + SQLConnection connection = DataStore.connectionPool.getConnection(); + Assertions.assertNotNull(connection); + + conversationDao = new ConversationDao(); + Assertions.assertNotNull(conversationDao); + } + + @Test + void getByOwner() throws Exception { + Assertions.assertTrue(conversationDao.getByOwner("test").isEmpty()); + } + + @Test + void storeObject() throws Exception { + Conversation conversation = new Conversation(); + Assertions.assertFalse(conversationDao.storeObject(conversation)); + } + + @Test + void storeCoList() throws Exception { + ArrayList<Conversation> conversationList = new ArrayList<>(); + Assertions.assertFalse(conversationDao.storeConversationList(conversationList)); + + Conversation conversation = new Conversation(); + conversationList.add(conversation); + Assertions.assertFalse(conversationDao.storeConversationList(conversationList)); + } +} diff --git a/datastore/src/test/java/net/jami/datastore/dao/ConversationRequestDaoTest.java b/datastore/src/test/java/net/jami/datastore/dao/ConversationRequestDaoTest.java new file mode 100644 index 0000000000000000000000000000000000000000..37162822d76d75b4fc7c4502de4b7fa7fe8ae997 --- /dev/null +++ b/datastore/src/test/java/net/jami/datastore/dao/ConversationRequestDaoTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 by Savoir-faire Linux + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package net.jami.datastore.dao; + +import net.jami.datastore.main.DataStore; +import net.jami.jams.common.dao.connectivity.SQLConnection; +import net.jami.jams.common.objects.conversations.ConversationRequest; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +class ConversationRequestDaoTest { + private ConversationRequestDao conversationRequestDao; + + @BeforeEach + void setUp() { + DataStore dataStore = new DataStore("jdbc:derby:memory:testdb;create=true"); + SQLConnection connection = DataStore.connectionPool.getConnection(); + Assertions.assertNotNull(connection); + + conversationRequestDao = new ConversationRequestDao(); + Assertions.assertNotNull(conversationRequestDao); + } + + @Test + void getByOwner() throws Exception { + Assertions.assertTrue(conversationRequestDao.getByOwner("test").isEmpty()); + } + + @Test + void storeObject() throws Exception { + ConversationRequest conversationRequest = new ConversationRequest(); + Assertions.assertFalse(conversationRequestDao.storeObject(conversationRequest)); + } + + @Test + void storeConversationRequestList() throws Exception { + ArrayList<ConversationRequest> conversationList = new ArrayList<>(); + Assertions.assertFalse( + conversationRequestDao.storeConversationRequestList(conversationList)); + + ConversationRequest conversationRequest = new ConversationRequest(); + conversationList.add(conversationRequest); + Assertions.assertFalse( + conversationRequestDao.storeConversationRequestList(conversationList)); + } +} diff --git a/jams-common/src/main/java/net/jami/jams/common/objects/conversations/Conversation.java b/jams-common/src/main/java/net/jami/jams/common/objects/conversations/Conversation.java new file mode 100644 index 0000000000000000000000000000000000000000..19f9969cc3309e51464ef2ffd81baf4314dd34b8 --- /dev/null +++ b/jams-common/src/main/java/net/jami/jams/common/objects/conversations/Conversation.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 by Savoir-faire Linux + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package net.jami.jams.common.objects.conversations; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import net.jami.jams.common.serialization.database.DatabaseObject; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +@Getter +@Setter +@NoArgsConstructor +@EqualsAndHashCode +public class Conversation implements DatabaseObject { + + private transient String owner; + + private String id; + private Long created; + private Long removed; + private Long erased; + private String members; + private String lastDisplayed; + + public Conversation(ResultSet rs) throws Exception { + this.owner = rs.getString("owner"); + this.id = rs.getString("id"); + this.created = rs.getLong("created"); + this.removed = rs.getLong("removed"); + this.erased = rs.getLong("erased"); + this.members = rs.getString("members"); + + this.lastDisplayed = rs.getString("lastDisplayed"); + } + + @Override + public PreparedStatement getInsert(PreparedStatement ps) throws Exception { + ps.setString(1, owner); + ps.setString(2, id); + ps.setLong(3, created); + ps.setLong(4, removed); + ps.setLong(5, erased); + ps.setString(6, members); + ps.setString(7, lastDisplayed); + return ps; + } + + @Override + public PreparedStatement getDelete(PreparedStatement ps) throws Exception { + return null; + } + + @Override + public PreparedStatement getUpdate(PreparedStatement ps) throws Exception { + ps.setLong(1, created); + ps.setLong(2, removed); + ps.setLong(3, erased); + ps.setString(4, members); + ps.setString(5, lastDisplayed); + ps.setString(6, owner); + ps.setString(7, id); + return ps; + } +} diff --git a/jams-common/src/main/java/net/jami/jams/common/objects/conversations/ConversationRequest.java b/jams-common/src/main/java/net/jami/jams/common/objects/conversations/ConversationRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..4df6495b13ba6dce49138589faa30e0c6eb86db3 --- /dev/null +++ b/jams-common/src/main/java/net/jami/jams/common/objects/conversations/ConversationRequest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 by Savoir-faire Linux + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package net.jami.jams.common.objects.conversations; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import net.jami.jams.common.serialization.database.DatabaseObject; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +@Getter +@Setter +@NoArgsConstructor +@EqualsAndHashCode +public class ConversationRequest implements DatabaseObject { + + private transient String owner; + + private String conversationId; + private String sender; // 'from' became sender because 'from' is a reserved keyword in SQL + private String metadatas; + private Long received; + private Long declined; + + public ConversationRequest(ResultSet rs) throws Exception { + this.owner = rs.getString("owner"); + this.conversationId = rs.getString("conversationId"); + this.sender = rs.getString("sender"); + this.metadatas = rs.getString("metadatas"); + this.received = rs.getLong("received"); + this.declined = rs.getLong("declined"); + } + + @Override + public PreparedStatement getInsert(PreparedStatement ps) throws Exception { + ps.setString(1, owner); + ps.setString(2, conversationId); + ps.setString(3, sender); + ps.setString(4, metadatas); + ps.setLong(5, received); + ps.setLong(6, declined); + return ps; + } + + @Override + public PreparedStatement getDelete(PreparedStatement ps) throws Exception { + return null; + } + + @Override + public PreparedStatement getUpdate(PreparedStatement ps) throws Exception { + ps.setString(1, sender); + ps.setString(2, metadatas); + ps.setLong(3, received); + ps.setLong(4, declined); + ps.setString(5, owner); + ps.setString(6, conversationId); + return ps; + } +} diff --git a/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/ContactAdapter.java b/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/ContactAdapter.java index 67daa5865107e328606216831733143f6dc22bc8..ff6b3d9b58a061b91e34ae344b38151ab1ab0afd 100644 --- a/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/ContactAdapter.java +++ b/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/ContactAdapter.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2023 by Savoir-faire Linux + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ package net.jami.jams.common.serialization.adapters; import com.google.gson.*; diff --git a/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/ConversationAdapter.java b/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/ConversationAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..f8d15be1fd2f8356454d994750916c34ff56f0fa --- /dev/null +++ b/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/ConversationAdapter.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 by Savoir-faire Linux + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package net.jami.jams.common.serialization.adapters; + +import com.google.gson.*; + +import net.jami.jams.common.objects.conversations.Conversation; + +import java.lang.reflect.Type; + +public class ConversationAdapter + implements JsonSerializer<Conversation>, JsonDeserializer<Conversation> { + + /** + * { + * "created" : 1701180312, + * "erased" : 1701180454, + * "id" : "63afe87b8b18a3cfc40a4524ebd441c4a7d2336a", + * "lastDisplayed" : "63afe87b8b18a3cfc40a4524ebd441c4a7d2336a", + * "members" : + * [ + * { + * "uri" : "7172ad8cf00c93235886d8ce1b2889638c0da68d" + * }, + * { + * "uri" : "bf82b61537582a53fb0a28553f77060cc7a332c3" + * } + * ], + * "removed" : 1701180454 + * } + */ + @Override + public Conversation deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + Gson gson = new Gson(); + JsonObject input = json.getAsJsonObject(); + Conversation conversation = new Conversation(); + conversation.setId(input.get("id").getAsString()); + conversation.setCreated(input.get("created").getAsLong()); + + long timeRemoved = 0L; + if (input.has("removed")) { + timeRemoved = input.get("removed").getAsLong(); + } + conversation.setRemoved(timeRemoved); + + long timeErased = 0L; + if (input.has("erased")) { + timeErased = input.get("erased").getAsLong(); + } + conversation.setErased(timeErased); + if (input.has("members")) { + conversation.setMembers(gson.toJson(input.get("members"))); + } + conversation.setLastDisplayed(input.get("lastDisplayed").getAsString()); + return conversation; + } + + @Override + public JsonElement serialize( + Conversation conversation, Type typeOfSrc, JsonSerializationContext context) { + JsonObject output = new JsonObject(); + output.addProperty("id", conversation.getId()); + output.addProperty("created", conversation.getCreated()); + output.addProperty("removed", conversation.getRemoved()); + output.addProperty("erased", conversation.getErased()); + JsonElement jsonMembers = JsonParser.parseString(conversation.getMembers()); + output.add("members", jsonMembers); + output.addProperty("lastDisplayed", conversation.getLastDisplayed()); + return output; + } +} diff --git a/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/ConversationRequestAdapter.java b/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/ConversationRequestAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..f8cc05c518b4ab9bd55a96fbe6daf7339ee9e58e --- /dev/null +++ b/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/ConversationRequestAdapter.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 by Savoir-faire Linux + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package net.jami.jams.common.serialization.adapters; + +import com.google.gson.*; + +import net.jami.jams.common.objects.conversations.ConversationRequest; + +import java.lang.reflect.Type; + +public class ConversationRequestAdapter + implements JsonSerializer<ConversationRequest>, JsonDeserializer<ConversationRequest> { + + /** + * { + * "conversationId" : "b158323bd68c7f71f606e0a4fb505f59a8212afe", + * "from" : "89c5c8d61df665f9dde97a23666772bccb658c22", + * "metadatas" : + * { + * "avatar" : "", + * "title" : " " + * }, + * "received" : 1701188208 + * } + * + */ + @Override + public ConversationRequest deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + Gson gson = new Gson(); + JsonObject input = json.getAsJsonObject(); + ConversationRequest conversationRequest = new ConversationRequest(); + conversationRequest.setConversationId(input.get("conversationId").getAsString()); + conversationRequest.setSender(input.get("from").getAsString()); + + conversationRequest.setMetadatas(gson.toJson(input.get("metadatas"))); + + long timeReceived = 0L; + if (input.has("received")) { + timeReceived = input.get("received").getAsLong(); + } + conversationRequest.setReceived(timeReceived); + + long timeDeclined = 0L; + if (input.has("timeDeclined")) { + timeDeclined = input.get("timeDeclined").getAsLong(); + } + conversationRequest.setDeclined(timeDeclined); + return conversationRequest; + } + + @Override + public JsonElement serialize( + ConversationRequest conversationRequest, + Type typeOfSrc, + JsonSerializationContext context) { + JsonObject output = new JsonObject(); + output.addProperty("conversationId", conversationRequest.getConversationId()); + output.addProperty("from", conversationRequest.getSender()); + JsonElement jsonMetadatas = JsonParser.parseString(conversationRequest.getMetadatas()); + output.add("metadatas", jsonMetadatas); + output.addProperty("received", conversationRequest.getReceived()); + output.addProperty("declined", conversationRequest.getDeclined()); + return output; + } +} diff --git a/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/GsonFactory.java b/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/GsonFactory.java index c295ae3c216658c2db688419e3073d3de2cc3c28..d157e3a30c8fbae20b93cbc9426f768e56eec2d2 100644 --- a/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/GsonFactory.java +++ b/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/GsonFactory.java @@ -4,6 +4,8 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import net.jami.jams.common.objects.contacts.Contact; +import net.jami.jams.common.objects.conversations.Conversation; +import net.jami.jams.common.objects.conversations.ConversationRequest; import org.bouncycastle.pkcs.PKCS10CertificationRequest; @@ -14,6 +16,9 @@ public class GsonFactory { public static Gson createGson() { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(Contact.class, new ContactAdapter()); + gsonBuilder.registerTypeAdapter(Conversation.class, new ConversationAdapter()); + gsonBuilder.registerTypeAdapter( + ConversationRequest.class, new ConversationRequestAdapter()); gsonBuilder.registerTypeAdapter(PKCS10CertificationRequest.class, new CSRDeserializer()); gsonBuilder.registerTypeAdapter(PrivateKey.class, new PrivateKeyAdapter()); gsonBuilder.registerTypeAdapter(X509Certificate.class, new X509CertificateAdapter()); diff --git a/jams-common/src/main/java/net/jami/jams/common/utils/ConversationMerger.java b/jams-common/src/main/java/net/jami/jams/common/utils/ConversationMerger.java new file mode 100644 index 0000000000000000000000000000000000000000..fdb48edeca35673142793b896b686e64ef5751c5 --- /dev/null +++ b/jams-common/src/main/java/net/jami/jams/common/utils/ConversationMerger.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 by Savoir-faire Linux + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package net.jami.jams.common.utils; + +import net.jami.jams.common.objects.conversations.Conversation; +import net.jami.jams.common.objects.conversations.ConversationRequest; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class ConversationMerger { + + public static List<Conversation> mergeConversations( + List<Conversation> remote, List<Conversation> local) { + List<Conversation> output = new ArrayList<>(); + final HashMap<String, Conversation[]> conversationMap = new HashMap<>(); + remote.forEach( + conversation -> { + conversationMap.putIfAbsent( + conversation.getId(), new Conversation[] {null, null}); + conversationMap.get(conversation.getId())[0] = conversation; + }); + local.forEach( + conversation -> { + conversationMap.putIfAbsent( + conversation.getId(), new Conversation[] {null, null}); + conversationMap.get(conversation.getId())[1] = conversation; + }); + conversationMap.forEach( + (k, v) -> { + if (v[0] == null) output.add(v[1]); + else if (v[1] == null) output.add(v[0]); + else { + // Merge policy we pick the conversation with the latest timestamp among + // created and removed + Conversation latestCreated = + v[0].getCreated() > v[1].getCreated() ? v[0] : v[1]; + Conversation latestRemoved = + v[0].getRemoved() > v[1].getRemoved() ? v[0] : v[1]; + if (latestCreated.getCreated() > latestRemoved.getRemoved()) { + output.add(latestCreated); + } else { + output.add(latestRemoved); + } + } + }); + return output; + } + + public static List<ConversationRequest> mergeConversationRequests( + List<ConversationRequest> remote, List<ConversationRequest> local) { + List<ConversationRequest> output = new ArrayList<>(); + final HashMap<String, ConversationRequest[]> conversationRequestMap = new HashMap<>(); + remote.forEach( + conversationRequest -> { + conversationRequestMap.putIfAbsent( + conversationRequest.getConversationId(), + new ConversationRequest[] {null, null}); + conversationRequestMap.get(conversationRequest.getConversationId())[0] = + conversationRequest; + }); + local.forEach( + conversationRequest -> { + conversationRequestMap.putIfAbsent( + conversationRequest.getConversationId(), + new ConversationRequest[] {null, null}); + conversationRequestMap.get(conversationRequest.getConversationId())[1] = + conversationRequest; + }); + conversationRequestMap.forEach( + (k, v) -> { + if (v[0] == null) output.add(v[1]); + else if (v[1] == null) output.add(v[0]); + else { + // Merge policy we pick the conversation request with the latest timestamp + // among + // received and declined + ConversationRequest latestReceived = + v[0].getReceived() > v[1].getReceived() ? v[0] : v[1]; + ConversationRequest latestDeclined = + v[0].getDeclined() > v[1].getDeclined() ? v[0] : v[1]; + if (latestReceived.getReceived() > latestDeclined.getDeclined()) { + output.add(latestReceived); + } else { + output.add(latestDeclined); + } + } + }); + return output; + } +} diff --git a/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/contacts/ContactServlet.java b/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/contacts/ContactServlet.java index 1fca80882a77c64ae4ffb3b62f60b380ab64fb61..7293c4b6a8e9ce47da5a53641dfada72f7b06b6d 100644 --- a/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/contacts/ContactServlet.java +++ b/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/contacts/ContactServlet.java @@ -161,7 +161,7 @@ public class ContactServlet extends HttpServlet { remoteList.forEach(contact -> contact.setOwner(owner)); List<Contact> result = ContactMerger.mergeContacts(localList, remoteList); - if (!dataStore.getContactDao().storeContactList(result)) + if (result.size() > 0 && !dataStore.getContactDao().storeContactList(result)) TomcatCustomErrorHandler.sendCustomError(resp, 500, "Could not store contacts!"); else { resp.getOutputStream().write(gson.toJson(result).getBytes()); diff --git a/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/conversations/ConversationRequestServlet.java b/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/conversations/ConversationRequestServlet.java new file mode 100644 index 0000000000000000000000000000000000000000..ba54781d8166d82e85a162f47a3e98b94e53b377 --- /dev/null +++ b/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/conversations/ConversationRequestServlet.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 by Savoir-faire Linux + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package net.jami.jams.server.servlets.api.auth.conversations; + +import static net.jami.jams.server.Server.dataStore; + +import com.google.gson.Gson; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import net.jami.jams.common.objects.conversations.ConversationRequest; +import net.jami.jams.common.serialization.adapters.GsonFactory; +import net.jami.jams.common.serialization.tomcat.TomcatCustomErrorHandler; +import net.jami.jams.common.utils.ConversationMerger; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +@WebServlet("/api/auth/conversationRequests") +public class ConversationRequestServlet extends HttpServlet { + private static final Gson gson = GsonFactory.createGson(); + + /** + * @apiVersion 1.0.0 + * @api {post} /api/auth/conversationRequests Sync conversationRequest list + * @apiName postConversationRequests + * @apiGroup ConversationRequests + * + * @apiParam {body} Conversation JSON representation of the conversationRequest object + * @apiParamExample {json} Request-Example: [ + * {"conversationId":"1231221","received":1594742298377}, + * {"conversationId":"213123","declined":1594742298377} ] + * + * @apiSuccess (200) {json} ConversationRequest[] user's converationRequests + * @apiError (500) {null} null contact could not be successfully added + */ + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + String owner = req.getAttribute("username").toString(); + addConversationRequests(req, resp, owner); + } + + public static void addConversationRequests( + HttpServletRequest req, HttpServletResponse resp, String owner) throws IOException { + List<ConversationRequest> localList = + dataStore.getConversationRequestDao().getByOwner(owner); + List<ConversationRequest> remoteList = + Arrays.asList(gson.fromJson(req.getReader(), ConversationRequest[].class)); + + remoteList.forEach(conversationRequest -> conversationRequest.setOwner(owner)); + List<ConversationRequest> result = + ConversationMerger.mergeConversationRequests(localList, remoteList); + + if (result.size() > 0 + && !dataStore.getConversationRequestDao().storeConversationRequestList(result)) + TomcatCustomErrorHandler.sendCustomError( + resp, 500, "Could not store conversationRequests!"); + else { + resp.getOutputStream().write(gson.toJson(result).getBytes()); + resp.flushBuffer(); + } + } +} diff --git a/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/conversations/ConversationServlet.java b/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/conversations/ConversationServlet.java new file mode 100644 index 0000000000000000000000000000000000000000..11dae5e5dbf0b2e05e5a8b3e92604f8058b46ce9 --- /dev/null +++ b/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/conversations/ConversationServlet.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 by Savoir-faire Linux + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package net.jami.jams.server.servlets.api.auth.conversations; + +import static net.jami.jams.server.Server.dataStore; + +import com.google.gson.Gson; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import net.jami.jams.common.objects.conversations.Conversation; +import net.jami.jams.common.serialization.adapters.GsonFactory; +import net.jami.jams.common.serialization.tomcat.TomcatCustomErrorHandler; +import net.jami.jams.common.utils.ConversationMerger; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +@WebServlet("/api/auth/conversations") +public class ConversationServlet extends HttpServlet { + private static final Gson gson = GsonFactory.createGson(); + + /** + * @apiVersion 1.0.0 + * @api {post} /api/auth/conversations Sync conversation list + * @apiName postConversations + * @apiGroup Conversations + * + * @apiParam {body} Conversation JSON representation of the conversation object + * @apiParamExample {json} Request-Example: [ + * {"id":"1231221","created":1594742298377}, + * {"id":"213123","removed":1594742298377} ] + * + * @apiSuccess (200) {json} Conversation[] user's conversations + * @apiError (500) {null} null conversation could not be successfully synced + */ + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + String owner = req.getAttribute("username").toString(); + addConversations(req, resp, owner); + } + + public static void addConversations( + HttpServletRequest req, HttpServletResponse resp, String owner) throws IOException { + List<Conversation> remoteList = + Arrays.asList(gson.fromJson(req.getReader(), Conversation[].class)); + List<Conversation> localList = dataStore.getConversationDao().getByOwner(owner); + remoteList.forEach(conversation -> conversation.setOwner(owner)); + List<Conversation> result = ConversationMerger.mergeConversations(localList, remoteList); + + if (result.size() > 0 && !dataStore.getConversationDao().storeConversationList(result)) + TomcatCustomErrorHandler.sendCustomError(resp, 500, "Could not store conversations!"); + else { + resp.getOutputStream().write(gson.toJson(result).getBytes()); + resp.flushBuffer(); + } + } +} diff --git a/jams-server/src/main/java/net/jami/jams/server/servlets/filters/ARequestLoggingFilter.java b/jams-server/src/main/java/net/jami/jams/server/servlets/filters/ARequestLoggingFilter.java index 412bedc66dc1155e8c2834363999cda037d5983e..1f97599876e15e58102c54c00963e1b0439827dd 100644 --- a/jams-server/src/main/java/net/jami/jams/server/servlets/filters/ARequestLoggingFilter.java +++ b/jams-server/src/main/java/net/jami/jams/server/servlets/filters/ARequestLoggingFilter.java @@ -19,7 +19,11 @@ public class ARequestLoggingFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = ((HttpServletRequest) request); - log.info("Request: {} {}", req.getMethod(), req.getRequestURI()); + log.info( + "Request: {} {} - Remote IP: {}", + req.getMethod(), + req.getRequestURI(), + request.getRemoteAddr()); chain.doFilter(request, response); } } diff --git a/ldap-connector/src/main/java/net/jami/jams/ldap/connector/service/AuthenticationService.java b/ldap-connector/src/main/java/net/jami/jams/ldap/connector/service/AuthenticationService.java index 7280fdccf002c09a78dbaf5aa3c6700253cbe109..a697bd42bd80e6db90b0c340e1cbe85e8c4b91c3 100644 --- a/ldap-connector/src/main/java/net/jami/jams/ldap/connector/service/AuthenticationService.java +++ b/ldap-connector/src/main/java/net/jami/jams/ldap/connector/service/AuthenticationService.java @@ -46,7 +46,8 @@ public class AuthenticationService { public boolean authenticateUser(String username, String password) { try { FormatDnResolver dnResolver = new FormatDnResolver(); - dnResolver.setFormat(settings.getUsernameField() + "=%s," + settings.getBaseDN()); + dnResolver.setFormat( + settings.getUsernameField() + "=%s,ou=users," + settings.getBaseDN()); SimpleBindAuthenticationHandler bindAuthenticationHandler = new SimpleBindAuthenticationHandler(connectionFactory); Authenticator auth = new Authenticator();