From 9b82f8ff694c258966f7629ff8cf5320e81ef903 Mon Sep 17 00:00:00 2001 From: Dejan Date: Tue, 13 Sep 2022 13:44:25 +0200 Subject: [PATCH 01/50] WIP --- Dockerfile | 24 ++++-- pom.xml | 45 +++++++---- recording.yaml | 2 +- script.txt | 12 --- .../wire/bots/recording/DAO/ChannelsDAO.java | 14 ++-- .../wire/bots/recording/DAO/EventsDAO.java | 14 ++-- .../recording/DAO/EventsResultSetMapper.java | 9 ++- .../recording/DAO/UUIDResultSetMapper.java | 8 +- .../wire/bots/recording/EventProcessor.java | 12 +-- .../wire/bots/recording/MessageHandler.java | 81 +++++++------------ .../java/com/wire/bots/recording/Service.java | 5 +- .../com/wire/bots/recording/model/Config.java | 2 +- .../com/wire/bots/recording/utils/Cache.java | 14 ++-- .../wire/bots/recording/utils/Collector.java | 5 +- .../com/wire/bots/recording/utils/Helper.java | 4 +- .../bots/recording/utils/ImagesBundle.java | 2 +- .../bots/recording/utils/InstantCache.java | 15 ++-- .../recording/ConversationTemplateTest.java | 46 +++++------ .../wire/bots/recording/utils/TestCache.java | 6 +- 19 files changed, 149 insertions(+), 171 deletions(-) delete mode 100644 script.txt diff --git a/Dockerfile b/Dockerfile index 60692da..c6a0038 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,14 +8,12 @@ WORKDIR /app COPY pom.xml ./ RUN mvn verify --fail-never -U -# build stuff +# build COPY . ./ RUN mvn -Dmaven.test.skip=true package -FROM dejankovacevic/bots.runtime:2.10.3 - -COPY --from=build /app/target/recording.jar /opt/recording/recording.jar -COPY --from=build /app/recording.yaml /etc/recording/recording.yaml +# runtime stage +FROM wirebot/runtime:1.2.0 RUN mkdir /opt/recording/assets RUN mkdir /opt/recording/avatars @@ -25,6 +23,18 @@ COPY --from=build /app/src/main/resources/assets/* /opt/recording/assets/ WORKDIR /opt/recording -EXPOSE 8080 8081 8082 +EXPOSE 8080 8081 + +# Copy configuration +COPY recording.yaml /opt/recording/ + +# Copy built target +COPY --from=build /app/target/recording.jar /opt/recording/ + +# create version file +ARG release_version=development +ENV RELEASE_FILE_PATH=/opt/recording/release.txt +RUN echo $release_version > $RELEASE_FILE_PATH -CMD ["sh", "-c","/usr/bin/java -Djava.library.path=/opt/wire/lib -jar recording.jar server /etc/recording/recording.yaml"] +EXPOSE 8080 8081 +ENTRYPOINT ["java", "-jar", "recording.jar", "server", "/opt/recording/recording.yaml"] diff --git a/pom.xml b/pom.xml index bf0001e..8ac41b0 100644 --- a/pom.xml +++ b/pom.xml @@ -8,25 +8,35 @@ com.wire.bots 0.3.0 - - - lithium - https://packagecloud.io/dkovacevic/lithium/maven2 - - true - - - + Recording Bot + Recording Bot Service For Wire + https://wire.com/ + + + + GNU General Public License v3.0 + https://www.gnu.org/licenses/gpl-3.0.en.html + repo + + - 1.0.2 + 11 + 11 + UTF-8 + UTF-8 + + 2.1.1 + 1.0.10 + 0.16.0 + true - com.wire.bots + com.wire lithium - 2.36.5 + 3.3.7 com.github.spullara.mustache.java @@ -80,16 +90,16 @@ org.apache.maven.plugins maven-compiler-plugin - 3.0 + 3.8.1 - 1.8 - 1.8 + 11 + 11 org.apache.maven.plugins maven-jar-plugin - 2.3.2 + 3.2.0 true @@ -97,7 +107,7 @@ org.apache.maven.plugins maven-shade-plugin - 2.4.3 + 3.2.4 recording false @@ -133,4 +143,5 @@ + diff --git a/recording.yaml b/recording.yaml index 8b89d36..ae27969 100644 --- a/recording.yaml +++ b/recording.yaml @@ -16,7 +16,7 @@ swagger: - http - https -auth: ${SERVICE_TOKEN:-} +token: ${SERVICE_TOKEN:-} email: ${WIRE_EMAIL:-} password: ${WIRE_PASSWORD:-} diff --git a/script.txt b/script.txt deleted file mode 100644 index 30e74b2..0000000 --- a/script.txt +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE Recording_Events ( - messageId UUID PRIMARY KEY, - conversationId UUID NOT NULL, - type VARCHAR NOT NULL, - payload VARCHAR NOT NULL, - time TIMESTAMP NOT NULL -); - -CREATE TABLE Recording_Channels ( - conversationId UUID PRIMARY KEY, - botId UUID -); diff --git a/src/main/java/com/wire/bots/recording/DAO/ChannelsDAO.java b/src/main/java/com/wire/bots/recording/DAO/ChannelsDAO.java index d03b69e..64700f0 100644 --- a/src/main/java/com/wire/bots/recording/DAO/ChannelsDAO.java +++ b/src/main/java/com/wire/bots/recording/DAO/ChannelsDAO.java @@ -1,9 +1,9 @@ package com.wire.bots.recording.DAO; -import org.skife.jdbi.v2.sqlobject.Bind; -import org.skife.jdbi.v2.sqlobject.SqlQuery; -import org.skife.jdbi.v2.sqlobject.SqlUpdate; -import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper; +import org.jdbi.v3.sqlobject.config.RegisterColumnMapper; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; import java.util.List; import java.util.UUID; @@ -15,15 +15,15 @@ int insert(@Bind("conversationId") UUID conversationId, @Bind("botId") UUID botId); @SqlQuery("SELECT conversationId AS UUID FROM Channels WHERE conversationId = :conversationId") - @RegisterMapper(UUIDResultSetMapper.class) + @RegisterColumnMapper(UUIDResultSetMapper.class) UUID contains(@Bind("conversationId") UUID conversationId); @SqlQuery("SELECT botId AS UUID FROM Channels WHERE conversationId = :conversationId") - @RegisterMapper(UUIDResultSetMapper.class) + @RegisterColumnMapper(UUIDResultSetMapper.class) UUID getBotId(@Bind("conversationId") UUID conversationId); @SqlQuery("SELECT conversationId AS UUID FROM Channels") - @RegisterMapper(UUIDResultSetMapper.class) + @RegisterColumnMapper(UUIDResultSetMapper.class) List listConversations(); @SqlUpdate("DELETE FROM Channels WHERE conversationId = :conversationId") diff --git a/src/main/java/com/wire/bots/recording/DAO/EventsDAO.java b/src/main/java/com/wire/bots/recording/DAO/EventsDAO.java index 250a427..71d35f5 100644 --- a/src/main/java/com/wire/bots/recording/DAO/EventsDAO.java +++ b/src/main/java/com/wire/bots/recording/DAO/EventsDAO.java @@ -1,10 +1,10 @@ package com.wire.bots.recording.DAO; import com.wire.bots.recording.model.Event; -import org.skife.jdbi.v2.sqlobject.Bind; -import org.skife.jdbi.v2.sqlobject.SqlQuery; -import org.skife.jdbi.v2.sqlobject.SqlUpdate; -import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper; +import org.jdbi.v3.sqlobject.config.RegisterColumnMapper; +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; import java.util.List; import java.util.UUID; @@ -19,18 +19,18 @@ int insert(@Bind("messageId") UUID messageId, @Bind("payload") String payload); @SqlQuery("SELECT * FROM Events WHERE messageId = :messageId") - @RegisterMapper(EventsResultSetMapper.class) + @RegisterColumnMapper(EventsResultSetMapper.class) Event get(@Bind("messageId") UUID messageId); @SqlUpdate("UPDATE Events SET payload = :payload, type = :type WHERE messageId = :messageId") int update(@Bind("messageId") UUID messageId, @Bind("type") String type, @Bind("payload") String payload); @SqlQuery("SELECT * FROM Events WHERE conversationId = :conversationId ORDER BY time ASC") - @RegisterMapper(EventsResultSetMapper.class) + @RegisterColumnMapper(EventsResultSetMapper.class) List listAllAsc(@Bind("conversationId") UUID conversationId); @SqlQuery("SELECT DISTINCT conversationId AS UUID FROM Events") - @RegisterMapper(UUIDResultSetMapper.class) + @RegisterColumnMapper(UUIDResultSetMapper.class) List listConversations(); @SqlUpdate("DELETE FROM Events WHERE messageId = :messageId") diff --git a/src/main/java/com/wire/bots/recording/DAO/EventsResultSetMapper.java b/src/main/java/com/wire/bots/recording/DAO/EventsResultSetMapper.java index b84067b..0c7168e 100644 --- a/src/main/java/com/wire/bots/recording/DAO/EventsResultSetMapper.java +++ b/src/main/java/com/wire/bots/recording/DAO/EventsResultSetMapper.java @@ -1,16 +1,17 @@ package com.wire.bots.recording.DAO; import com.wire.bots.recording.model.Event; -import org.skife.jdbi.v2.StatementContext; -import org.skife.jdbi.v2.tweak.ResultSetMapper; + +import org.jdbi.v3.core.mapper.ColumnMapper; +import org.jdbi.v3.core.statement.StatementContext; import java.sql.ResultSet; import java.sql.SQLException; import java.util.UUID; -public class EventsResultSetMapper implements ResultSetMapper { +public class EventsResultSetMapper implements ColumnMapper { @Override - public Event map(int i, ResultSet rs, StatementContext statementContext) throws SQLException { + public Event map(ResultSet rs, int columnNumber, StatementContext ctx) throws SQLException { Event event = new Event(); Object conversationId = rs.getObject("conversationId"); if (conversationId != null) diff --git a/src/main/java/com/wire/bots/recording/DAO/UUIDResultSetMapper.java b/src/main/java/com/wire/bots/recording/DAO/UUIDResultSetMapper.java index 664483e..d255f8a 100644 --- a/src/main/java/com/wire/bots/recording/DAO/UUIDResultSetMapper.java +++ b/src/main/java/com/wire/bots/recording/DAO/UUIDResultSetMapper.java @@ -1,15 +1,15 @@ package com.wire.bots.recording.DAO; -import org.skife.jdbi.v2.StatementContext; -import org.skife.jdbi.v2.tweak.ResultSetMapper; +import org.jdbi.v3.core.mapper.ColumnMapper; +import org.jdbi.v3.core.statement.StatementContext; import java.sql.ResultSet; import java.sql.SQLException; import java.util.UUID; -public class UUIDResultSetMapper implements ResultSetMapper { +public class UUIDResultSetMapper implements ColumnMapper { @Override - public UUID map(int i, ResultSet rs, StatementContext statementContext) throws SQLException { + public UUID map(ResultSet rs, int columnNumber, StatementContext ctx) throws SQLException { Object uuid = rs.getObject("uuid"); if (uuid != null) return (UUID) uuid; diff --git a/src/main/java/com/wire/bots/recording/EventProcessor.java b/src/main/java/com/wire/bots/recording/EventProcessor.java index e261853..9fd87f4 100644 --- a/src/main/java/com/wire/bots/recording/EventProcessor.java +++ b/src/main/java/com/wire/bots/recording/EventProcessor.java @@ -4,12 +4,12 @@ import com.wire.bots.recording.model.Event; import com.wire.bots.recording.utils.Cache; import com.wire.bots.recording.utils.Collector; -import com.wire.bots.sdk.WireClient; -import com.wire.bots.sdk.models.*; -import com.wire.bots.sdk.server.model.Member; -import com.wire.bots.sdk.server.model.SystemMessage; -import com.wire.bots.sdk.server.model.User; -import com.wire.bots.sdk.tools.Logger; +import com.wire.xenon.WireClient; +import com.wire.xenon.backend.models.Member; +import com.wire.xenon.backend.models.SystemMessage; +import com.wire.xenon.backend.models.User; +import com.wire.xenon.models.*; +import com.wire.xenon.tools.Logger; import java.io.File; import java.io.IOException; diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index 1bd71c2..7490cf2 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -6,16 +6,20 @@ import com.wire.bots.recording.DAO.EventsDAO; import com.wire.bots.recording.model.Event; import com.wire.bots.recording.utils.PdfGenerator; -import com.wire.bots.sdk.ClientRepo; -import com.wire.bots.sdk.MessageHandlerBase; -import com.wire.bots.sdk.WireClient; -import com.wire.bots.sdk.models.*; -import com.wire.bots.sdk.server.model.SystemMessage; -import com.wire.bots.sdk.tools.Logger; -import com.wire.bots.sdk.tools.Util; +import com.wire.lithium.ClientRepo; +import com.wire.xenon.MessageHandlerBase; +import com.wire.xenon.WireClient; +import com.wire.xenon.assets.FileAsset; +import com.wire.xenon.assets.FileAssetPreview; +import com.wire.xenon.assets.MessageText; +import com.wire.xenon.backend.models.SystemMessage; +import com.wire.xenon.models.*; +import com.wire.xenon.tools.Logger; +import com.wire.xenon.tools.Util; import java.io.File; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.UUID; @@ -63,8 +67,8 @@ void warmup(ClientRepo repo) { @Override public void onNewConversation(WireClient client, SystemMessage msg) { try { - client.sendText(WELCOME_LABEL); - client.sendDirectText(HELP, msg.from); + client.send(new MessageText(WELCOME_LABEL)); + client.send(new MessageText(HELP), msg.from); } catch (Exception e) { Logger.error("onNewConversation: %s %s", client.getId(), e); } @@ -90,7 +94,7 @@ public void onMemberJoin(WireClient client, SystemMessage msg) { try { Logger.info("onMemberJoin: %s, bot: %s, user: %s", msg.type, botId, memberId); - client.sendDirectText(WELCOME_LABEL, memberId); + client.send(new MessageText(WELCOME_LABEL), memberId); //collector.sendPDF(memberId, "file:/opt"); //todo fix this } catch (Exception e) { Logger.error("onMemberJoin: %s %s", botId, e); @@ -200,37 +204,22 @@ public void onDelete(WireClient client, DeletedTextMessage msg) { } @Override - public void onImage(WireClient client, ImageMessage msg) { + public void onAssetData(WireClient client, RemoteMessage msg) { UUID convId = client.getConversationId(); UUID messageId = msg.getMessageId(); UUID botId = client.getId(); UUID userId = msg.getUserId(); - String type = "conversation.otr-message-add.new-image"; + String type = "conversation.otr-message-add.asset-data"; try { persist(convId, userId, botId, messageId, type, msg); } catch (Exception e) { - Logger.error("onImage: %s %s %s", botId, messageId, e); + Logger.error("onAssetData: %s %s %s", botId, messageId, e); } } @Override - public void onVideo(WireClient client, VideoMessage msg) { - UUID convId = client.getConversationId(); - UUID messageId = msg.getMessageId(); - UUID botId = client.getId(); - UUID userId = msg.getUserId(); - String type = "conversation.otr-message-add.new-video"; - - try { - persist(convId, userId, botId, messageId, type, msg); - } catch (Exception e) { - Logger.error("onVideo: %s %s %s", botId, messageId, e); - } - } - - @Override - public void onVideoPreview(WireClient client, ImageMessage msg) { + public void onVideoPreview(WireClient client, VideoPreviewMessage msg) { UUID convId = client.getConversationId(); UUID messageId = UUID.randomUUID(); UUID botId = client.getId(); @@ -258,21 +247,6 @@ public void onLinkPreview(WireClient client, LinkPreviewMessage msg) { } } - @Override - public void onAttachment(WireClient client, AttachmentMessage msg) { - UUID convId = client.getConversationId(); - UUID botId = client.getId(); - UUID messageId = msg.getMessageId(); - UUID userId = msg.getUserId(); - String type = "conversation.otr-message-add.new-attachment"; - - try { - persist(convId, userId, botId, messageId, type, msg); - } catch (Exception e) { - Logger.error("onAttachment: %s %s %s", botId, messageId, e); - } - } - @Override public void onReaction(WireClient client, ReactionMessage msg) { UUID convId = client.getConversationId(); @@ -326,16 +300,13 @@ private void generateHtml(WireClient client, UUID botId, UUID convId) { } private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, String cmd) throws Exception { -// if (!state.getState().origin.id.equals(userId)) -// return false; - switch (cmd) { case "/help": { - client.sendDirectText(HELP, userId); + client.send(new MessageText(HELP), userId); return true; } case "/pdf": { - client.sendDirectText("Generating PDF...", userId); + client.send(new MessageText("Generating PDF..."), userId); String filename = String.format("html/%s.html", convId); List events = eventsDAO.listAllAsc(convId); @@ -343,15 +314,19 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, String html = Util.readFile(file); String convName = client.getConversation().name; - String pdfFilename = String.format("html/%s.pdf", URLEncoder.encode(convName, "UTF-8")); + String pdfFilename = String.format("html/%s.pdf", URLEncoder.encode(convName, StandardCharsets.UTF_8)); File pdfFile = PdfGenerator.save(pdfFilename, html, "file:/opt"); - client.sendDirectFile(pdfFile, "application/pdf", userId); + + UUID messageId = UUID.randomUUID(); + String mimeType = "application/pdf"; + client.send(new FileAssetPreview(pdfFile.getName(), mimeType, pdfFile.length(), messageId), userId); + client.send(new FileAsset(messageId, mimeType), userId); return true; } case "/public": { channelsDAO.insert(convId, botId); String text = String.format("https://recording.services.wire.com/channel/%s.html", convId); - client.sendText(text, userId); + client.send(new MessageText(text), userId); return true; } case "/private": { @@ -359,7 +334,7 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, String filename = String.format("html/%s.html", convId); boolean delete = new File(filename).delete(); String txt = String.format("%s deleted: %s", filename, delete); - client.sendDirectText(txt, userId); + client.send(new MessageText(txt), userId); return true; } } diff --git a/src/main/java/com/wire/bots/recording/Service.java b/src/main/java/com/wire/bots/recording/Service.java index e9945bc..fa707db 100644 --- a/src/main/java/com/wire/bots/recording/Service.java +++ b/src/main/java/com/wire/bots/recording/Service.java @@ -21,8 +21,9 @@ import com.wire.bots.recording.DAO.EventsDAO; import com.wire.bots.recording.model.Config; import com.wire.bots.recording.utils.ImagesBundle; -import com.wire.bots.sdk.MessageHandlerBase; -import com.wire.bots.sdk.Server; + +import com.wire.lithium.Server; +import com.wire.xenon.MessageHandlerBase; import io.dropwizard.Application; import io.dropwizard.assets.AssetsBundle; import io.dropwizard.setup.Bootstrap; diff --git a/src/main/java/com/wire/bots/recording/model/Config.java b/src/main/java/com/wire/bots/recording/model/Config.java index f3b317d..e6f14fc 100644 --- a/src/main/java/com/wire/bots/recording/model/Config.java +++ b/src/main/java/com/wire/bots/recording/model/Config.java @@ -19,7 +19,7 @@ package com.wire.bots.recording.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.wire.bots.sdk.Configuration; +import com.wire.lithium.Configuration; import javax.validation.constraints.NotNull; diff --git a/src/main/java/com/wire/bots/recording/utils/Cache.java b/src/main/java/com/wire/bots/recording/utils/Cache.java index 10d9c56..49921a5 100644 --- a/src/main/java/com/wire/bots/recording/utils/Cache.java +++ b/src/main/java/com/wire/bots/recording/utils/Cache.java @@ -1,14 +1,12 @@ package com.wire.bots.recording.utils; import com.wire.bots.recording.Service; -import com.wire.bots.sdk.WireClient; -import com.wire.bots.sdk.exceptions.HttpException; -import com.wire.bots.sdk.models.MessageAssetBase; -import com.wire.bots.sdk.server.model.User; -import com.wire.bots.sdk.tools.Logger; -import com.wire.bots.sdk.user.API; -import com.wire.bots.sdk.user.LoginClient; -import com.wire.bots.sdk.user.model.Access; +import com.wire.lithium.API; +import com.wire.xenon.WireClient; +import com.wire.xenon.backend.models.User; +import com.wire.xenon.exceptions.HttpException; +import com.wire.xenon.models.MessageAssetBase; +import com.wire.xenon.tools.Logger; import java.io.File; import java.util.UUID; diff --git a/src/main/java/com/wire/bots/recording/utils/Collector.java b/src/main/java/com/wire/bots/recording/utils/Collector.java index 3c72987..b2e6ced 100644 --- a/src/main/java/com/wire/bots/recording/utils/Collector.java +++ b/src/main/java/com/wire/bots/recording/utils/Collector.java @@ -3,9 +3,8 @@ import com.github.mustachejava.DefaultMustacheFactory; import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheFactory; -import com.wire.bots.sdk.models.*; -import com.wire.bots.sdk.server.model.Asset; -import com.wire.bots.sdk.server.model.User; +import com.wire.xenon.backend.models.User; +import com.wire.xenon.models.*; import javax.annotation.Nullable; import java.io.*; diff --git a/src/main/java/com/wire/bots/recording/utils/Helper.java b/src/main/java/com/wire/bots/recording/utils/Helper.java index e79bbe8..fdb3bf3 100644 --- a/src/main/java/com/wire/bots/recording/utils/Helper.java +++ b/src/main/java/com/wire/bots/recording/utils/Helper.java @@ -1,7 +1,7 @@ package com.wire.bots.recording.utils; -import com.wire.bots.sdk.models.MessageAssetBase; -import com.wire.bots.sdk.tools.Logger; +import com.wire.xenon.models.MessageAssetBase; +import com.wire.xenon.tools.Logger; import org.commonmark.Extension; import org.commonmark.ext.autolink.AutolinkExtension; import org.commonmark.node.Node; diff --git a/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java b/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java index c28edc2..199135a 100644 --- a/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java +++ b/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java @@ -1,6 +1,6 @@ package com.wire.bots.recording.utils; -import com.wire.bots.sdk.tools.Logger; +import com.wire.xenon.tools.Logger; import io.dropwizard.assets.AssetsBundle; import io.dropwizard.servlets.assets.AssetServlet; diff --git a/src/main/java/com/wire/bots/recording/utils/InstantCache.java b/src/main/java/com/wire/bots/recording/utils/InstantCache.java index 0253a05..bee6c16 100644 --- a/src/main/java/com/wire/bots/recording/utils/InstantCache.java +++ b/src/main/java/com/wire/bots/recording/utils/InstantCache.java @@ -1,14 +1,11 @@ package com.wire.bots.recording.utils; - -import com.wire.bots.sdk.exceptions.HttpException; -import com.wire.bots.sdk.models.MessageAssetBase; -import com.wire.bots.sdk.server.model.User; -import com.wire.bots.sdk.tools.Logger; -import com.wire.bots.sdk.tools.Util; -import com.wire.bots.sdk.user.API; -import com.wire.bots.sdk.user.LoginClient; -import com.wire.bots.sdk.user.model.Access; +import com.wire.lithium.API; +import com.wire.xenon.backend.models.User; +import com.wire.xenon.exceptions.HttpException; +import com.wire.xenon.models.MessageAssetBase; +import com.wire.xenon.tools.Logger; +import com.wire.xenon.tools.Util; import javax.ws.rs.client.Client; import java.security.MessageDigest; diff --git a/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java b/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java index 00e3f3e..b5444b7 100644 --- a/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java +++ b/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java @@ -3,9 +3,9 @@ import com.wire.bots.recording.utils.Collector; import com.wire.bots.recording.utils.PdfGenerator; import com.wire.bots.recording.utils.TestCache; -import com.wire.bots.sdk.models.*; -import com.wire.bots.sdk.tools.Logger; -import com.wire.bots.sdk.tools.Util; +import com.wire.xenon.models.*; +import com.wire.xenon.tools.Logger; +import com.wire.xenon.tools.Util; import org.junit.Test; import java.io.File; @@ -18,70 +18,68 @@ public class ConversationTemplateTest { private static final String SRC_TEST_OUT = "src/test/resources"; private static TextMessage txt(UUID userId, String time, String text) { - TextMessage ret = new TextMessage(UUID.randomUUID(), UUID.randomUUID(), "", userId); + TextMessage ret = new TextMessage(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), "", userId, time); ret.setText(text); - ret.setTime(time); return ret; } private static ReactionMessage like(UUID userId, String emoji, String time, UUID msgId) { - ReactionMessage ret = new ReactionMessage(UUID.randomUUID(), UUID.randomUUID(), "", userId); + ReactionMessage ret = new ReactionMessage(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), "", userId, time); ret.setReactionMessageId(msgId); ret.setEmoji(emoji); - ret.setTime(time); return ret; } private static TextMessage quote(UUID userId, String text, String time, UUID msgId) { - TextMessage ret = new TextMessage(UUID.randomUUID(), UUID.randomUUID(), "", userId); + TextMessage ret = new TextMessage(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), "", userId, time); ret.setQuotedMessageId(msgId); ret.setText(text); - ret.setTime(time); return ret; } private static EditedTextMessage edit(UUID userId, String text, String time) { - EditedTextMessage ret = new EditedTextMessage(UUID.randomUUID(), UUID.randomUUID(), "", userId); + EditedTextMessage ret = new EditedTextMessage(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), "", userId, time); ret.setText(text); - ret.setTime(time); return ret; } private static ImageMessage img(UUID userId, String time, String key, String mimeType) { - ImageMessage ret = new ImageMessage(UUID.randomUUID(), UUID.randomUUID(), "", userId); + ImageMessage ret = new ImageMessage(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), "", userId, time); ret.setAssetKey(key); ret.setMimeType(mimeType); - ret.setTime(time); return ret; } private static VideoMessage vid(UUID userId, String time, String key, String mimeType) { - VideoMessage ret = new VideoMessage(UUID.randomUUID(), UUID.randomUUID(), "", userId); + VideoMessage ret = new VideoMessage(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), "", userId, time); ret.setAssetKey(key); ret.setMimeType(mimeType); - ret.setTime(time); ret.setHeight(568); ret.setWidth(320); return ret; } private static AttachmentMessage attachment(UUID userId, String time, String key, String name, String mimeType) { - AttachmentMessage ret = new AttachmentMessage(UUID.randomUUID(), UUID.randomUUID(), "", userId); + AttachmentMessage ret = new AttachmentMessage(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), "", userId, time); ret.setAssetKey(key); ret.setMimeType(mimeType); - ret.setTime(time); ret.setName(name); return ret; } private static LinkPreviewMessage link(UUID userId, String time, String text, String title, String url, String preview) { - LinkPreviewMessage ret = new LinkPreviewMessage(UUID.randomUUID(), UUID.randomUUID(), "", userId); - ret.setTime(time); - ret.setTitle(title); - ret.setText(text); - ret.setUrl(url); - ret.setAssetKey(preview); - ret.setMimeType("image/png"); + LinkPreviewMessage ret = new LinkPreviewMessage( + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID(), + "", + userId, + time, + title, + text, + url, + preview, + "image/png") return ret; } diff --git a/src/test/java/com/wire/bots/recording/utils/TestCache.java b/src/test/java/com/wire/bots/recording/utils/TestCache.java index bb8ba7c..cccfc2b 100644 --- a/src/test/java/com/wire/bots/recording/utils/TestCache.java +++ b/src/test/java/com/wire/bots/recording/utils/TestCache.java @@ -1,9 +1,9 @@ package com.wire.bots.recording.utils; import com.wire.bots.recording.ConversationTemplateTest; -import com.wire.bots.sdk.models.MessageAssetBase; -import com.wire.bots.sdk.server.model.Asset; -import com.wire.bots.sdk.server.model.User; +import com.wire.xenon.backend.models.Asset; +import com.wire.xenon.backend.models.User; +import com.wire.xenon.models.MessageAssetBase; import java.io.File; import java.util.ArrayList; From b5c9bb6759c9ed74e0537914bab0e7de88f36343 Mon Sep 17 00:00:00 2001 From: Dejan Date: Tue, 13 Sep 2022 17:08:15 +0200 Subject: [PATCH 02/50] WIP --- .../wire/bots/recording/EventProcessor.java | 28 ++++++++------- .../wire/bots/recording/MessageHandler.java | 35 +++++++++++++++++-- .../com/wire/bots/recording/utils/Cache.java | 12 +++---- .../wire/bots/recording/utils/Collector.java | 32 ++++++++--------- .../com/wire/bots/recording/utils/Helper.java | 6 ++-- .../bots/recording/utils/InstantCache.java | 8 ++--- .../recording/ConversationTemplateTest.java | 22 ------------ 7 files changed, 77 insertions(+), 66 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/EventProcessor.java b/src/main/java/com/wire/bots/recording/EventProcessor.java index 9fd87f4..3cf42f8 100644 --- a/src/main/java/com/wire/bots/recording/EventProcessor.java +++ b/src/main/java/com/wire/bots/recording/EventProcessor.java @@ -63,29 +63,27 @@ private void add(WireClient client, Collector collector, Event event, boolean wi collector.add(message); } break; - case "conversation.otr-message-add.new-attachment": { - AttachmentMessage message = mapper.readValue(event.payload, AttachmentMessage.class); + case "conversation.otr-message-add.asset-data": { + RemoteMessage message = mapper.readValue(event.payload, RemoteMessage.class); collector.add(message); } break; - case "conversation.otr-message-add.new-image": { - ImageMessage message = mapper.readValue(event.payload, ImageMessage.class); + case "conversation.otr-message-add.file-preview": { + FilePreviewMessage message = mapper.readValue(event.payload, FilePreviewMessage.class); collector.add(message); } break; - case "conversation.otr-message-add.new-preview": { - if (withPreviews) { - ImageMessage message = mapper.readValue(event.payload, ImageMessage.class); - collector.add(message); - } + case "conversation.otr-message-add.image-preview": { + PhotoPreviewMessage message = mapper.readValue(event.payload, PhotoPreviewMessage.class); + collector.add(message); } break; - case "conversation.otr-message-add.new-video": { + case "conversation.otr-message-add.video-preview": { VideoMessage message = mapper.readValue(event.payload, VideoMessage.class); collector.add(message); } break; - case "conversation.otr-message-add.new-link": { + case "conversation.otr-message-add.link-preview": { LinkPreviewMessage message = mapper.readValue(event.payload, LinkPreviewMessage.class); collector.addLink(message); } @@ -147,11 +145,15 @@ private void add(WireClient client, Collector collector, Event event, boolean wi break; } } catch (Exception e) { - //e.printStackTrace(); - Logger.error("EventProcessor.add: msg: %s `%s` err: %s", event.messageId, event.type, e); + Logger.exception(e, "EventProcessor.add: msg: %s `%s`", event.messageId, event.type); } } + class Asset { + RemoteMessage remote; + OriginMessage preview; + } + private String formatConversation(SystemMessage msg, Cache cache, WireClient client) { StringBuilder sb = new StringBuilder(); User user = cache.getUser(msg.from); diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index 7490cf2..1cc9f21 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -218,13 +218,28 @@ public void onAssetData(WireClient client, RemoteMessage msg) { } } + @Override + public void onFilePreview(WireClient client, FilePreviewMessage msg) { + UUID convId = client.getConversationId(); + UUID messageId = UUID.randomUUID(); + UUID botId = client.getId(); + UUID userId = msg.getUserId(); + String type = "conversation.otr-message-add.file-preview"; + + try { + persist(convId, userId, botId, messageId, type, msg); + } catch (Exception e) { + Logger.exception(e, "onFilePreview: %s %s", botId, messageId); + } + } + @Override public void onVideoPreview(WireClient client, VideoPreviewMessage msg) { UUID convId = client.getConversationId(); UUID messageId = UUID.randomUUID(); UUID botId = client.getId(); UUID userId = msg.getUserId(); - String type = "conversation.otr-message-add.new-preview"; + String type = "conversation.otr-message-add.video-preview"; try { persist(convId, userId, botId, messageId, type, msg); @@ -233,12 +248,28 @@ public void onVideoPreview(WireClient client, VideoPreviewMessage msg) { } } + @Override + public void onPhotoPreview(WireClient client, PhotoPreviewMessage msg) { + UUID convId = client.getConversationId(); + UUID messageId = UUID.randomUUID(); + UUID botId = client.getId(); + UUID userId = msg.getUserId(); + String type = "conversation.otr-message-add.image-preview"; + + try { + persist(convId, userId, botId, messageId, type, msg); + } catch (Exception e) { + Logger.exception(e, "onPhotoPreview: %s %s", botId, messageId); + } + } + + @Override public void onLinkPreview(WireClient client, LinkPreviewMessage msg) { UUID convId = client.getConversationId(); UUID messageId = msg.getMessageId(); UUID botId = client.getId(); UUID userId = msg.getUserId(); - String type = "conversation.otr-message-add.new-link"; + String type = "conversation.otr-message-add.link-preview"; try { persist(convId, userId, botId, messageId, type, msg); diff --git a/src/main/java/com/wire/bots/recording/utils/Cache.java b/src/main/java/com/wire/bots/recording/utils/Cache.java index 49921a5..88cf63b 100644 --- a/src/main/java/com/wire/bots/recording/utils/Cache.java +++ b/src/main/java/com/wire/bots/recording/utils/Cache.java @@ -6,6 +6,7 @@ import com.wire.xenon.backend.models.User; import com.wire.xenon.exceptions.HttpException; import com.wire.xenon.models.MessageAssetBase; +import com.wire.xenon.models.RemoteMessage; import com.wire.xenon.tools.Logger; import java.io.File; @@ -27,14 +28,13 @@ public static void clear(UUID userId) { profiles.remove(userId); } - File getAssetFile(MessageAssetBase message) { - return assetsMap.computeIfAbsent(message.getAssetKey(), k -> { + File getAssetFile(RemoteMessage message) { + return assetsMap.computeIfAbsent(message.getAssetId(), k -> { try { byte[] image = downloadAsset(message); return Helper.saveAsset(image, message); } catch (Exception e) { - Logger.error("Cache.getAssetFile: %s", e); - return Helper.assetFile(message.getAssetKey(), message.getMimeType()); + throw new RuntimeException(e); } }); } @@ -51,8 +51,8 @@ File getProfileImage(String key) { }); } - protected byte[] downloadAsset(MessageAssetBase message) throws Exception { - return client.downloadAsset(message.getAssetKey(), + protected byte[] downloadAsset(RemoteMessage message) throws Exception { + return client.downloadAsset(message.getAssetId(), message.getAssetToken(), message.getSha256(), message.getOtrKey()); diff --git a/src/main/java/com/wire/bots/recording/utils/Collector.java b/src/main/java/com/wire/bots/recording/utils/Collector.java index b2e6ced..2dd96e0 100644 --- a/src/main/java/com/wire/bots/recording/utils/Collector.java +++ b/src/main/java/com/wire/bots/recording/utils/Collector.java @@ -72,7 +72,7 @@ private static String toDate(String timestamp) throws ParseException { return df.format(date); } - public Sender add(TextMessage event) throws ParseException { + public void add(TextMessage event) throws ParseException { Message message = new Message(); message.id = event.getMessageId(); @@ -84,7 +84,7 @@ public Sender add(TextMessage event) throws ParseException { Sender sender = sender(event.getUserId()); sender.add(message); - return append(sender, message, event.getTime()); + append(sender, message, event.getTime()); } private String extractYouTube(String text) { @@ -95,12 +95,12 @@ private String extractYouTube(String text) { return null; } - public Sender addEdit(EditedTextMessage event) throws ParseException { + public void addEdit(EditedTextMessage event) throws ParseException { addSystem("✏ Edited", event.getTime(), "", event.getMessageId()); - return add(event); + add(event); } - public Sender add(ImageMessage event) throws ParseException { + public void add(RemoteMessage event, PhotoPreviewMessage preview) throws ParseException { Message message = new Message(); message.id = event.getMessageId(); message.timeStamp = event.getTime(); @@ -111,10 +111,10 @@ public Sender add(ImageMessage event) throws ParseException { Sender sender = sender(event.getUserId()); sender.add(message); - return append(sender, message, event.getTime()); + append(sender, message, event.getTime()); } - public void add(VideoMessage event) throws ParseException { + public void add(RemoteMessage event, VideoPreviewMessage preview) throws ParseException { Message message = new Message(); message.id = event.getMessageId(); message.timeStamp = event.getTime(); @@ -122,9 +122,9 @@ public void add(VideoMessage event) throws ParseException { File file = cache.getAssetFile(event); message.video = new Video(); message.video.url = getFilename(file); - message.video.width = event.getWidth(); - message.video.height = event.getHeight(); - message.video.mimeType = event.getMimeType(); + message.video.width = preview.getWidth(); + message.video.height = preview.getHeight(); + message.video.mimeType = preview.getMimeType(); Sender sender = sender(event.getUserId()); sender.add(message); @@ -132,7 +132,7 @@ public void add(VideoMessage event) throws ParseException { append(sender, message, event.getTime()); } - public Sender add(AttachmentMessage event) throws ParseException { + public Sender add(RemoteMessage event, FilePreviewMessage preview) throws ParseException { Message message = new Message(); message.id = event.getMessageId(); message.timeStamp = event.getTime(); @@ -141,7 +141,7 @@ public Sender add(AttachmentMessage event) throws ParseException { String assetFilename = getFilename(file); message.attachment = new Attachment(); - message.attachment.name = String.format("%s (%s)", event.getName(), event.getAssetKey()); + message.attachment.name = String.format("%s (%s)", preview.getName(), event.getAssetId()); message.attachment.url = "file://" + assetFilename; Sender sender = sender(event.getUserId()); @@ -202,9 +202,10 @@ public void addLink(LinkPreviewMessage event) throws ParseException { * @return true if the message was added * @throws ParseException */ - public boolean addSystem(String text, String dateTime, String type, UUID msgId) throws ParseException { - if (lastMessage != null && lastMessage.timeStamp.equals(dateTime)) - return false; + public void addSystem(String text, String dateTime, String type, UUID msgId) throws ParseException { + if (lastMessage != null && lastMessage.timeStamp.equals(dateTime)) { + return; + } Message message = new Message(); message.id = msgId; @@ -215,7 +216,6 @@ public boolean addSystem(String text, String dateTime, String type, UUID msgId) sender.add(message); append(sender, message, dateTime); - return true; } private String getText(TextMessage event) { diff --git a/src/main/java/com/wire/bots/recording/utils/Helper.java b/src/main/java/com/wire/bots/recording/utils/Helper.java index fdb3bf3..f955d95 100644 --- a/src/main/java/com/wire/bots/recording/utils/Helper.java +++ b/src/main/java/com/wire/bots/recording/utils/Helper.java @@ -1,6 +1,6 @@ package com.wire.bots.recording.utils; -import com.wire.xenon.models.MessageAssetBase; +import com.wire.xenon.models.RemoteMessage; import com.wire.xenon.tools.Logger; import org.commonmark.Extension; import org.commonmark.ext.autolink.AutolinkExtension; @@ -32,8 +32,8 @@ static File getProfile(byte[] profile, String key) throws Exception { return save(profile, file); } - static File saveAsset(byte[] image, MessageAssetBase message) throws Exception { - File file = assetFile(message.getAssetKey(), message.getMimeType()); + static File saveAsset(byte[] image, RemoteMessage message) throws Exception { + File file = assetFile(message.getAssetId(), message.getMimeType()); return save(image, file); } diff --git a/src/main/java/com/wire/bots/recording/utils/InstantCache.java b/src/main/java/com/wire/bots/recording/utils/InstantCache.java index bee6c16..ebd4617 100644 --- a/src/main/java/com/wire/bots/recording/utils/InstantCache.java +++ b/src/main/java/com/wire/bots/recording/utils/InstantCache.java @@ -3,7 +3,7 @@ import com.wire.lithium.API; import com.wire.xenon.backend.models.User; import com.wire.xenon.exceptions.HttpException; -import com.wire.xenon.models.MessageAssetBase; +import com.wire.xenon.models.RemoteMessage; import com.wire.xenon.tools.Logger; import com.wire.xenon.tools.Util; @@ -51,15 +51,15 @@ protected User getUserObject(UUID userId) { } @Override - protected byte[] downloadAsset(MessageAssetBase message) throws Exception { + protected byte[] downloadAsset(RemoteMessage message) throws Exception { byte[] cipher; try { - cipher = api.downloadAsset(message.getAssetKey(), message.getAssetToken()); + cipher = api.downloadAsset(message.getAssetId(), message.getAssetToken()); } catch (HttpException e) { if (e.getCode() == 401) { Access access = new LoginClient(client).login(email, password); this.api = new API(client, null, access.getToken()); - cipher = api.downloadAsset(message.getAssetKey(), message.getAssetToken()); + cipher = api.downloadAsset(message.getAssetId(), message.getAssetToken()); } else { throw e; } diff --git a/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java b/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java index b5444b7..f9bc464 100644 --- a/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java +++ b/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java @@ -67,22 +67,6 @@ private static AttachmentMessage attachment(UUID userId, String time, String key return ret; } - private static LinkPreviewMessage link(UUID userId, String time, String text, String title, String url, String preview) { - LinkPreviewMessage ret = new LinkPreviewMessage( - UUID.randomUUID(), - UUID.randomUUID(), - UUID.randomUUID(), - "", - userId, - time, - title, - text, - url, - preview, - "image/png") - return ret; - } - //@Before public void clean() { String pdf = getFilename(CONV_NAME, "pdf"); @@ -157,12 +141,6 @@ public void templateTest() throws Exception { " laborum.")); collector.add(txt(dejan, saturday, "This is some url [google](https://google.com)")); collector.add(txt(dejan, saturday, "https://wire.com")); - collector.addLink(link(dejan, - saturday, - "Yo, check this link preview: https://wire.com. Totally without bugs!", - "The most secure collaboration platform · Wire", - "wire.com", - "logo")); collector.add(txt(dejan, saturday, "This is some url https://google.com and some text")); collector.add(txt(dejan, saturday, "These two urls https://google.com https://wire.com")); collector.addSystem("**Dejo** removed **Lipis**", saturday2, "conversation.member-leave", UUID.randomUUID()); From 14fdad9fa9c46be8cb5dc7afc3de89a05bc53cd6 Mon Sep 17 00:00:00 2001 From: Dejan Date: Tue, 13 Sep 2022 20:14:40 +0200 Subject: [PATCH 03/50] it compiles --- .../wire/bots/recording/EventProcessor.java | 11 +-- .../com/wire/bots/recording/utils/Cache.java | 70 +--------------- .../wire/bots/recording/utils/Collector.java | 65 ++++++++------- .../com/wire/bots/recording/utils/Helper.java | 2 +- .../bots/recording/utils/InstantCache.java | 83 ------------------- 5 files changed, 38 insertions(+), 193 deletions(-) delete mode 100644 src/main/java/com/wire/bots/recording/utils/InstantCache.java diff --git a/src/main/java/com/wire/bots/recording/EventProcessor.java b/src/main/java/com/wire/bots/recording/EventProcessor.java index 3cf42f8..e08eb9b 100644 --- a/src/main/java/com/wire/bots/recording/EventProcessor.java +++ b/src/main/java/com/wire/bots/recording/EventProcessor.java @@ -70,22 +70,17 @@ private void add(WireClient client, Collector collector, Event event, boolean wi break; case "conversation.otr-message-add.file-preview": { FilePreviewMessage message = mapper.readValue(event.payload, FilePreviewMessage.class); - collector.add(message); + //collector.add(message); } break; case "conversation.otr-message-add.image-preview": { PhotoPreviewMessage message = mapper.readValue(event.payload, PhotoPreviewMessage.class); - collector.add(message); + //collector.add(message); } break; case "conversation.otr-message-add.video-preview": { VideoMessage message = mapper.readValue(event.payload, VideoMessage.class); - collector.add(message); - } - break; - case "conversation.otr-message-add.link-preview": { - LinkPreviewMessage message = mapper.readValue(event.payload, LinkPreviewMessage.class); - collector.addLink(message); + //collector.add(message); } break; case "conversation.member-join": { diff --git a/src/main/java/com/wire/bots/recording/utils/Cache.java b/src/main/java/com/wire/bots/recording/utils/Cache.java index 88cf63b..0585fae 100644 --- a/src/main/java/com/wire/bots/recording/utils/Cache.java +++ b/src/main/java/com/wire/bots/recording/utils/Cache.java @@ -1,13 +1,9 @@ package com.wire.bots.recording.utils; -import com.wire.bots.recording.Service; -import com.wire.lithium.API; import com.wire.xenon.WireClient; import com.wire.xenon.backend.models.User; import com.wire.xenon.exceptions.HttpException; -import com.wire.xenon.models.MessageAssetBase; import com.wire.xenon.models.RemoteMessage; -import com.wire.xenon.tools.Logger; import java.io.File; import java.util.UUID; @@ -16,7 +12,6 @@ public class Cache { private static final ConcurrentHashMap assetsMap = new ConcurrentHashMap<>();// private static final ConcurrentHashMap users = new ConcurrentHashMap<>();// - private static final ConcurrentHashMap profiles = new ConcurrentHashMap<>();// private final WireClient client; public Cache(WireClient client) { @@ -25,7 +20,6 @@ public Cache(WireClient client) { public static void clear(UUID userId) { users.remove(userId); - profiles.remove(userId); } File getAssetFile(RemoteMessage message) { @@ -39,18 +33,6 @@ File getAssetFile(RemoteMessage message) { }); } - File getProfileImage(String key) { - return assetsMap.computeIfAbsent(key, k -> { - try { - byte[] profile = downloadProfilePicture(key); - return Helper.getProfile(profile, key); - } catch (Exception e) { - Logger.error("Cache.getProfileImage: key: %s, ex: %s", key, e); - return new File(Helper.avatarFile(key)); - } - }); - } - protected byte[] downloadAsset(RemoteMessage message) throws Exception { return client.downloadAsset(message.getAssetId(), message.getAssetToken(), @@ -62,62 +44,12 @@ protected User getUserInternal(UUID userId) throws HttpException { return client.getUser(userId); } - protected byte[] downloadProfilePicture(String key) throws Exception { - return client.downloadProfilePicture(key); - } - - public User getProfile(UUID userId) { - return profiles.computeIfAbsent(userId, k -> getUserObject(userId)); - } - - protected User getUserObject(UUID userId) { - String email = Service.instance.getConfig().email; - String password = Service.instance.getConfig().password; - - LoginClient loginClient = new LoginClient(Service.instance.getClient()); - try { - Access access = getAccess(email, password, loginClient); - API api = new API(Service.instance.getClient(), null, access.getToken()); - return api.getUser(userId); - } catch (Exception e) { - Logger.error("Cache.getUserObject: userId: %s, ex: %s", userId, e); - User ret = new User(); - ret.id = userId; - ret.name = userId.toString(); - return ret; - } - } - - private Access getAccess(String email, String password, LoginClient loginClient) throws HttpException, InterruptedException { - int retries = 1; - HttpException exception = null; - while (retries < 5) { - try { - return loginClient.login(email, password); - } catch (HttpException e) { - exception = e; - - if (e.getCode() != 420) - break; - - Logger.warning("getAccess: %s, %d, retrying...", e.getMessage(), e.getCode()); - retries++; - Thread.sleep(5 * 1000); - } - } - throw exception; - } - public User getUser(UUID userId) { return users.computeIfAbsent(userId, k -> { try { return getUserInternal(userId); } catch (Exception e) { - Logger.error("Cache.getUser: userId: %s, ex: %s", userId, e); - User ret = new User(); - ret.id = userId; - ret.name = userId.toString(); - return ret; + throw new RuntimeException(e); } }); } diff --git a/src/main/java/com/wire/bots/recording/utils/Collector.java b/src/main/java/com/wire/bots/recording/utils/Collector.java index 2dd96e0..0f6f372 100644 --- a/src/main/java/com/wire/bots/recording/utils/Collector.java +++ b/src/main/java/com/wire/bots/recording/utils/Collector.java @@ -3,6 +3,7 @@ import com.github.mustachejava.DefaultMustacheFactory; import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheFactory; +import com.wire.xenon.backend.models.Asset; import com.wire.xenon.backend.models.User; import com.wire.xenon.models.*; @@ -100,7 +101,7 @@ public void addEdit(EditedTextMessage event) throws ParseException { add(event); } - public void add(RemoteMessage event, PhotoPreviewMessage preview) throws ParseException { + public void add(RemoteMessage event) throws ParseException { Message message = new Message(); message.id = event.getMessageId(); message.timeStamp = event.getTime(); @@ -168,28 +169,28 @@ public void add(ReactionMessage event) { } } - public void addLink(LinkPreviewMessage event) throws ParseException { - Message message = new Message(); - message.id = event.getMessageId(); - message.timeStamp = event.getTime(); - message.text = Helper.markdown2Html(event.getText()); - - Link link = new Link(); - link.title = event.getTitle(); - link.summary = event.getSummary(); - link.url = event.getUrl(); - - File file = cache.getAssetFile(event); - if (file.exists()) - link.preview = getFilename(file); - - message.link = link; - - Sender sender = sender(event.getUserId()); - sender.add(message); - - append(sender, message, event.getTime()); - } +// public void addLink(LinkPreviewMessage event) throws ParseException { +// Message message = new Message(); +// message.id = event.getMessageId(); +// message.timeStamp = event.getTime(); +// message.text = Helper.markdown2Html(event.getText()); +// +// Link link = new Link(); +// link.title = event.getTitle(); +// link.summary = event.getSummary(); +// link.url = event.getUrl(); +// +// File file = cache.getAssetFile(event); +// if (file.exists()) +// link.preview = getFilename(file); +// +// message.link = link; +// +// Sender sender = sender(event.getUserId()); +// sender.add(message); +// +// append(sender, message, event.getTime()); +// } /** * Adds new message with _name_ `system` and avatar based on _type_. If the last message has the same timestamp as @@ -306,22 +307,22 @@ private String getFilename(File file) { @Nullable private String getAvatar(UUID userId) { - User user = cache.getProfile(userId); - String profileAssetKey = getProfileAssetKey(user); - if (profileAssetKey != null) { - File file = cache.getProfileImage(profileAssetKey); - return String.format("/%s/%s", "avatars", file.getName()); - } +// User user = cache.getProfile(userId); +// String profileAssetKey = getProfileAssetKey(user.assets); +// if (profileAssetKey != null) { +// File file = cache.getProfileImage(profileAssetKey); +// return String.format("/%s/%s", "avatars", file.getName()); +// } return null; } @Nullable - private String getProfileAssetKey(User user) { - if (user.assets == null) { + private String getProfileAssetKey(@Nullable ArrayList assets) { + if (assets == null) { return null; } - for (Asset asset : user.assets) { + for (Asset asset : assets) { if (asset.size.equals("preview")) { return asset.key; } diff --git a/src/main/java/com/wire/bots/recording/utils/Helper.java b/src/main/java/com/wire/bots/recording/utils/Helper.java index f955d95..408be82 100644 --- a/src/main/java/com/wire/bots/recording/utils/Helper.java +++ b/src/main/java/com/wire/bots/recording/utils/Helper.java @@ -33,7 +33,7 @@ static File getProfile(byte[] profile, String key) throws Exception { } static File saveAsset(byte[] image, RemoteMessage message) throws Exception { - File file = assetFile(message.getAssetId(), message.getMimeType()); + File file = assetFile(message.getAssetId(), "image/jpeg"); return save(image, file); } diff --git a/src/main/java/com/wire/bots/recording/utils/InstantCache.java b/src/main/java/com/wire/bots/recording/utils/InstantCache.java deleted file mode 100644 index ebd4617..0000000 --- a/src/main/java/com/wire/bots/recording/utils/InstantCache.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.wire.bots.recording.utils; - -import com.wire.lithium.API; -import com.wire.xenon.backend.models.User; -import com.wire.xenon.exceptions.HttpException; -import com.wire.xenon.models.RemoteMessage; -import com.wire.xenon.tools.Logger; -import com.wire.xenon.tools.Util; - -import javax.ws.rs.client.Client; -import java.security.MessageDigest; -import java.util.Arrays; -import java.util.UUID; - -public class InstantCache extends Cache { - private final String email; - private final String password; - private final Client client; - private API api; - - public InstantCache(String email, String password, Client client) throws HttpException { - super(null); - this.email = email; - this.password = password; - this.client = client; - Access access = new LoginClient(client).login(email, password); - this.api = new API(client, null, access.getToken()); - } - - public UUID getUserId(String handle) { - try { - return this.api.getUserId(handle); - } catch (HttpException e) { - Logger.error("InstantCache.getUserId: username: %s, ex: %s", handle, e); - e.printStackTrace(); - } - return null; - } - - @Override - protected User getUserObject(UUID userId) { - try { - return api.getUser(userId); - } catch (Exception e) { - Logger.error("InstantCache.getUserObject: userId: %s, ex: %s", userId, e); - User ret = new User(); - ret.id = userId; - ret.name = userId.toString(); - return ret; - } - } - - @Override - protected byte[] downloadAsset(RemoteMessage message) throws Exception { - byte[] cipher; - try { - cipher = api.downloadAsset(message.getAssetId(), message.getAssetToken()); - } catch (HttpException e) { - if (e.getCode() == 401) { - Access access = new LoginClient(client).login(email, password); - this.api = new API(client, null, access.getToken()); - cipher = api.downloadAsset(message.getAssetId(), message.getAssetToken()); - } else { - throw e; - } - } - byte[] sha256 = MessageDigest.getInstance("SHA-256").digest(cipher); - if (!Arrays.equals(sha256, message.getSha256())) - throw new Exception("Failed sha256 check"); - - return Util.decrypt(message.getOtrKey(), cipher); - } - - @Override - protected User getUserInternal(UUID userId) throws HttpException { - return api.getUser(userId); - } - - @Override - protected byte[] downloadProfilePicture(String key) throws Exception { - return api.downloadAsset(key, null); - } -} From 5e892b40eb71beaf2860ed74fdbab7a2b9834358 Mon Sep 17 00:00:00 2001 From: Dejan Date: Thu, 22 Sep 2022 11:25:43 +0200 Subject: [PATCH 04/50] Config --- recording.yaml | 4 ++-- src/main/java/com/wire/bots/recording/MessageHandler.java | 2 +- src/main/java/com/wire/bots/recording/model/Config.java | 5 +---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/recording.yaml b/recording.yaml index ae27969..27c0c92 100644 --- a/recording.yaml +++ b/recording.yaml @@ -17,8 +17,8 @@ swagger: - https token: ${SERVICE_TOKEN:-} -email: ${WIRE_EMAIL:-} -password: ${WIRE_PASSWORD:-} +apiHost: ${WIRE_API_HOST:-https://prod-nginz-https.wire.com} +url: ${PUBLIC_URL:-https://recording.services.wire.com} database: driverClass: org.postgresql.Driver diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index 1cc9f21..cc14ccb 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -356,7 +356,7 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, } case "/public": { channelsDAO.insert(convId, botId); - String text = String.format("https://recording.services.wire.com/channel/%s.html", convId); + String text = String.format("%s/channel/%s.html", Service.instance.getConfig().url, convId); client.send(new MessageText(text), userId); return true; } diff --git a/src/main/java/com/wire/bots/recording/model/Config.java b/src/main/java/com/wire/bots/recording/model/Config.java index e6f14fc..7becf9d 100644 --- a/src/main/java/com/wire/bots/recording/model/Config.java +++ b/src/main/java/com/wire/bots/recording/model/Config.java @@ -26,8 +26,5 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class Config extends Configuration { @NotNull - public String email; - - @NotNull - public String password; + public String url; } From ef05907998d87ef986a5ec8df79acd6552f63c64 Mon Sep 17 00:00:00 2001 From: Dejan Date: Thu, 22 Sep 2022 11:56:54 +0200 Subject: [PATCH 05/50] Fixed tests, bumped deps --- pom.xml | 5 +++-- .../recording/ConversationTemplateTest.java | 10 +++++----- .../wire/bots/recording/utils/TestCache.java | 18 +++++------------- 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/pom.xml b/pom.xml index 8ac41b0..8400dab 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,8 @@ UTF-8 UTF-8 - 2.1.1 + 2.1.2 + 3.4.0 1.0.10 0.16.0 true @@ -36,7 +37,7 @@ com.wire lithium - 3.3.7 + ${lithium.version} com.github.spullara.mustache.java diff --git a/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java b/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java index f9bc464..3762717 100644 --- a/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java +++ b/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java @@ -117,15 +117,15 @@ public void templateTest() throws Exception { collector.addSystem("**Dejo** deleted something", friday2, "conversation.otr-message-add.delete-text", UUID.randomUUID()); collector.add(txt(lipis, saturday, "8")); collector.add(quote(dejan, "This was a quote", saturday, seven.getMessageId())); - collector.add(img(lipis, saturday, "ognjiste2", "image/png")); - collector.add(img(lipis, saturday, "small", "image/png")); + //collector.add(img(lipis, saturday, "ognjiste2", "image/png")); + //collector.add(img(lipis, saturday, "small", "image/png")); collector.add(txt(dejan, saturday, "9")); collector.add(txt(dejan, saturday, "10")); collector.add(txt(lipis, saturday, "```This is some cool Java code here```")); collector.add(txt(dejan, saturday, "12")); collector.add(txt(lipis, saturday, "13")); - collector.add(img(dejan, saturday, "ognjiste", "image/png")); - collector.add(attachment(lipis, saturday, "Wire+Security+Whitepaper", "Wire Security Paper.pdf", "pdf")); + //collector.add(img(dejan, saturday, "ognjiste", "image/png")); + //collector.add(attachment(lipis, saturday, "Wire+Security+Whitepaper", "Wire Security Paper.pdf", "pdf")); collector.add(txt(lipis, saturday, "15")); collector.add(txt(dejan, saturday, "Lorem ipsum **dolor** sit amet, consectetur adipiscing elit, sed " + "do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam," + @@ -145,7 +145,7 @@ public void templateTest() throws Exception { collector.add(txt(dejan, saturday, "These two urls https://google.com https://wire.com")); collector.addSystem("**Dejo** removed **Lipis**", saturday2, "conversation.member-leave", UUID.randomUUID()); collector.add(txt(dejan, saturday, "https://www.youtube.com/watch?v=rlR4PJn8b8I")); - collector.add(vid(dejan, saturday, "panormos", "video/mp4")); + // collector.add(vid(dejan, saturday, "panormos", "video/mp4")); Collector.Conversation conversation = collector.getConversation(); File htmlFile = collector.executeFile(getFilename(conversation.getTitle(), "html")); diff --git a/src/test/java/com/wire/bots/recording/utils/TestCache.java b/src/test/java/com/wire/bots/recording/utils/TestCache.java index cccfc2b..b0ed7fb 100644 --- a/src/test/java/com/wire/bots/recording/utils/TestCache.java +++ b/src/test/java/com/wire/bots/recording/utils/TestCache.java @@ -3,7 +3,7 @@ import com.wire.bots.recording.ConversationTemplateTest; import com.wire.xenon.backend.models.Asset; import com.wire.xenon.backend.models.User; -import com.wire.xenon.models.MessageAssetBase; +import com.wire.xenon.models.RemoteMessage; import java.io.File; import java.util.ArrayList; @@ -16,7 +16,7 @@ public TestCache() { } @Override - public User getProfile(UUID userId) { + protected User getUserInternal(UUID userId) { User ret = new User(); ret.id = userId; ret.assets = new ArrayList<>(); @@ -38,19 +38,11 @@ public User getProfile(UUID userId) { @Override public User getUser(UUID userId) { - return getProfile(userId); + return getUserInternal(userId); } @Override - File getProfileImage(String key) { - return new File(String.format("src/test/resources/avatars/%s.png", key)); - } - - @Override - File getAssetFile(MessageAssetBase message) { - String extension = Helper.getExtension(message.getMimeType()); - return new File(String.format("src/test/resources/images/%s.%s", - message.getAssetKey(), - extension)); + File getAssetFile(RemoteMessage message) { + return new File(String.format("src/test/resources/avatars/%s.png", message.getAssetId())); } } From ece8faeea1983d69a221e24b52b0889aa8949dc5 Mon Sep 17 00:00:00 2001 From: Dejan Date: Thu, 22 Sep 2022 12:36:24 +0200 Subject: [PATCH 06/50] Fixed dockerfile --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c6a0038..118f1c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM maven:3.6.3-jdk-8-slim AS build +FROM maven:3-openjdk-11 AS build LABEL description="Wire Recording bot" LABEL project="wire-bots:recording" @@ -15,6 +15,7 @@ RUN mvn -Dmaven.test.skip=true package # runtime stage FROM wirebot/runtime:1.2.0 +RUN mkdir /opt/recording RUN mkdir /opt/recording/assets RUN mkdir /opt/recording/avatars RUN mkdir /opt/recording/html From c017544ff6920b0f4eb61d32cebc20ed31cd7352 Mon Sep 17 00:00:00 2001 From: Amit Sagtani Date: Thu, 22 Sep 2022 17:39:53 +0530 Subject: [PATCH 07/50] Add docker compose based deployment (#24) * add docker-compose file for deploying recording bot * remove appender var from docker-compose --- docker-compose.yml | 69 ++++++++++++++++++++++++++++++++++++++++++++++ filebeat.yml | 21 ++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 docker-compose.yml create mode 100755 filebeat.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b5edfbc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,69 @@ +version: '3.8' +services: + app: + build: + context: . + dockerfile: Dockerfile + args: + - release_version=docker-compose + image: recordingbot + ports: + - 8080:8080 + - 8081:8081 + environment: + # put here the token used for testing + - SERVICE_TOKEN=some-service-token-from-BE + - WIRE_API_HOST="" + - PUBLIC_URL=http://localhost:8080 + # local database + - DB_URL=jdbc:postgresql://db:5432/recording + - DB_USER=recording + - DB_PASSWORD=recording + depends_on: + - db + - elasticsearch + - kibana + - filebeat + + db: + image: postgres:13 + # just for local development + environment: + - POSTGRES_USER=recording + - POSTGRES_PASSWORD=recording + - POSTGRES_DB=recording + ports: + - 5432:5432 + volumes: + - recording-db:/var/lib/postgresql/data/ + + elasticsearch: + container_name: elasticsearch + image: docker.elastic.co/elasticsearch/elasticsearch:7.11.0 + environment: + - "ES_JAVA_OPTS=-Xms1g -Xmx1g" + - "discovery.type=single-node" + ports: + - 9200:9200 + + kibana: + container_name: kb-container + image: docker.elastic.co/kibana/kibana:7.11.0 + environment: + - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 + - ELASTICSEARCH_URL=http://elasticsearch:9200 + depends_on: + - elasticsearch + ports: + - 5601:5601 + + filebeat: + image: "docker.elastic.co/beats/filebeat:7.2.0" + user: root + volumes: + - ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro + - /var/lib/docker:/var/lib/docker:ro + - /var/run/docker.sock:/var/run/docker.sock + +volumes: + recording-db: \ No newline at end of file diff --git a/filebeat.yml b/filebeat.yml new file mode 100755 index 0000000..bc442f9 --- /dev/null +++ b/filebeat.yml @@ -0,0 +1,21 @@ +filebeat.inputs: +- type: container + paths: + - '/var/lib/docker/containers/*/*.log' + +processors: +- add_docker_metadata: + host: "unix:///var/run/docker.sock" + +- decode_json_fields: + fields: ["message"] + target: "json" + overwrite_keys: true + +output.elasticsearch: + hosts: ["elasticsearch:9200"] + indices: + - index: "filebeat-%{[agent.version]}-%{+yyyy.MM.dd}" + +logging.json: true +logging.metrics.enabled: false \ No newline at end of file From 7c1b1abb70d44624424e381ba2eb7cd3a01baa7e Mon Sep 17 00:00:00 2001 From: Dejan Date: Thu, 22 Sep 2022 14:25:35 +0200 Subject: [PATCH 08/50] Dep bump --- pom.xml | 29 ++++++++-------- .../wire/bots/recording/utils/UrlUtil.java | 34 ------------------- 2 files changed, 14 insertions(+), 49 deletions(-) delete mode 100644 src/main/java/com/wire/bots/recording/utils/UrlUtil.java diff --git a/pom.xml b/pom.xml index 8400dab..bea292d 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,10 @@ 3.4.0 1.0.10 0.16.0 + 0.17.0 + 0.9.10 + 2.11.1 + true @@ -42,28 +46,17 @@ com.github.spullara.mustache.java compiler - 0.9.5 + ${spullara.version} com.atlassian.commonmark commonmark - 0.12.1 + ${commonmark.version} com.atlassian.commonmark commonmark-ext-autolink - 0.12.1 - - - org.jsoup - jsoup - 1.14.2 - - - junit - junit - 4.13.1 - test + ${commonmark.version} com.openhtmltopdf @@ -83,7 +76,13 @@ net.lingala.zip4j zip4j - 2.9.1 + ${zip4j.version} + + + junit + junit + 4.13.2 + test diff --git a/src/main/java/com/wire/bots/recording/utils/UrlUtil.java b/src/main/java/com/wire/bots/recording/utils/UrlUtil.java deleted file mode 100644 index 58ebe81..0000000 --- a/src/main/java/com/wire/bots/recording/utils/UrlUtil.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.wire.bots.recording.utils; - -import org.jsoup.Connection; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.select.Elements; - -import javax.annotation.Nullable; -import java.io.IOException; - -class UrlUtil { - @Nullable - static String extractPagePreview(String url) throws IOException { - Connection con = Jsoup.connect(url); - Document doc = con.get(); - - Elements metaOgImage = doc.select("meta[property=og:image]"); - if (metaOgImage != null) { - return metaOgImage.attr("content"); - } - return null; - } - - static String extractPageTitle(String url) throws IOException { - Connection con = Jsoup.connect(url); - Document doc = con.get(); - - Elements title = doc.select("meta[property=og:title]"); - if (title != null) { - return title.attr("content"); - } - return doc.title(); - } -} From ce9e744da82ade621b1850cb4f4d6bac76775aee Mon Sep 17 00:00:00 2001 From: Dejan Date: Tue, 27 Sep 2022 10:15:39 +0200 Subject: [PATCH 09/50] workflows --- .github/workflows/staging.yml | 85 +++++++++++++++++++++++++++++++++++ docker-compose.yml | 2 +- 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/staging.yml diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml new file mode 100644 index 0000000..138898f --- /dev/null +++ b/.github/workflows/staging.yml @@ -0,0 +1,85 @@ +name: Staging Deployment + +on: + push: + branches: + - staging + +env: + # set docker image for the service - i.e. "wire-bot/poll" + DOCKER_IMAGE: wire-bot/recording-bot + # name of the service in the Dagobah - the value for label name, i.e. "polls" + SERVICE_NAME: recording-bot + +jobs: + publish: + name: Deploy to staging + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + # use latest tag as release version in the docker container + - name: Set Release Version + run: echo "RELEASE_VERSION=${GITHUB_SHA}" >> $GITHUB_ENV + + # extract metadata for labels https://github.com/crazy-max/ghaction-docker-meta + - name: Docker meta + id: docker_meta + uses: crazy-max/ghaction-docker-meta@v1 + with: + images: eu.gcr.io/${{ env.DOCKER_IMAGE }} + + # setup docker actions https://github.com/docker/build-push-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + # login to GCR repo + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + registry: eu.gcr.io + username: _json_key + password: ${{ secrets.GCR_ACCESS_JSON }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} + push: true + build-args: | + release_version=${{ env.RELEASE_VERSION }} + + # Setup gcloud CLI + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v0 + with: + service_account_email: kubernetes-deployment-agent@wire-bot.iam.gserviceaccount.com + service_account_key: ${{ secrets.GKE_SA_KEY }} + project_id: wire-bot + export_default_credentials: true + + # Configure Docker to use the gcloud command-line tool + - name: Configure Docker Google cloud + run: | + gcloud --quiet auth configure-docker + + # Get the GKE credentials so we can deploy to the cluster + - name: Obtain k8s credentials + env: + GKE_CLUSTER: dagobah + GKE_ZONE: europe-west1-c + run: | + gcloud container clusters get-credentials "$GKE_CLUSTER" --zone "$GKE_ZONE" + + # K8s is set up, deploy the app + - name: Deploy the Service + env: + SERVICE: ${{ env.SERVICE_NAME }} + run: | + kubectl delete pod -l app=$SERVICE -n staging + kubectl describe pod -l app=$SERVICE -n staging + + diff --git a/docker-compose.yml b/docker-compose.yml index b5edfbc..4ba6c68 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: environment: # put here the token used for testing - SERVICE_TOKEN=some-service-token-from-BE - - WIRE_API_HOST="" +# - WIRE_API_HOST= - PUBLIC_URL=http://localhost:8080 # local database - DB_URL=jdbc:postgresql://db:5432/recording From e584901f5ef55fdfd9060defcba989624ea8db76 Mon Sep 17 00:00:00 2001 From: Dejan Date: Tue, 27 Sep 2022 11:39:50 +0200 Subject: [PATCH 10/50] kibana for txt --- .../wire/bots/recording/MessageHandler.java | 37 ++++++++++++++++++- .../com/wire/bots/recording/utils/Helper.java | 9 +++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index cc14ccb..4358425 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -5,6 +5,7 @@ import com.wire.bots.recording.DAO.ChannelsDAO; import com.wire.bots.recording.DAO.EventsDAO; import com.wire.bots.recording.model.Event; +import com.wire.bots.recording.model.Log; import com.wire.bots.recording.utils.PdfGenerator; import com.wire.lithium.ClientRepo; import com.wire.xenon.MessageHandlerBase; @@ -12,17 +13,24 @@ import com.wire.xenon.assets.FileAsset; import com.wire.xenon.assets.FileAssetPreview; import com.wire.xenon.assets.MessageText; +import com.wire.xenon.backend.models.Conversation; +import com.wire.xenon.backend.models.Member; import com.wire.xenon.backend.models.SystemMessage; +import com.wire.xenon.exceptions.HttpException; import com.wire.xenon.models.*; import com.wire.xenon.tools.Logger; import com.wire.xenon.tools.Util; import java.io.File; +import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.text.ParseException; import java.util.List; import java.util.UUID; +import static com.wire.bots.recording.utils.Helper.date; + public class MessageHandler extends MessageHandlerBase { private final ObjectMapper mapper = new ObjectMapper(); @@ -160,9 +168,10 @@ public void onText(WireClient client, TextMessage msg) { return; persist(convId, userId, botId, messageId, type, msg); + + kibana(type, msg, client); } catch (Exception e) { - e.printStackTrace(); - Logger.error("OnText: %s ex: %s", client.getId(), e); + Logger.exception(e, "OnText: %s", client.getId()); } } @@ -396,4 +405,28 @@ private void persist(UUID convId, UUID senderId, UUID userId, UUID msgId, String throw new RuntimeException(error); } } + + void kibana(String type, TextMessage msg, WireClient client) throws IOException, HttpException, ParseException { + Log.Kibana kibana = new Log.Kibana(); + kibana.id = msg.getEventId(); + kibana.type = type; + kibana.messageID = msg.getMessageId(); + kibana.conversationID = msg.getConversationId(); + kibana.from = msg.getUserId(); + kibana.sent = date(msg.getTime()); + kibana.text = msg.getText(); + + kibana.sender = client.getUser(msg.getUserId()).handle; + + Conversation conversation = client.getConversation(); + kibana.conversationName = conversation.name; + + for (Member m : conversation.members) { + kibana.participants.add(client.getUser(m.id).handle); + } + + Log log = new Log(); + log.securehold = kibana; + System.out.println(mapper.writeValueAsString(log)); + } } diff --git a/src/main/java/com/wire/bots/recording/utils/Helper.java b/src/main/java/com/wire/bots/recording/utils/Helper.java index 408be82..9c565fd 100644 --- a/src/main/java/com/wire/bots/recording/utils/Helper.java +++ b/src/main/java/com/wire/bots/recording/utils/Helper.java @@ -13,7 +13,10 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Collections; +import java.util.Date; import java.util.List; public class Helper { @@ -73,4 +76,10 @@ static String markdown2Html(@Nullable String text) { .build() .render(document); } + + public static Long date(@Nullable String date) throws ParseException { + SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + Date ret = parser.parse(date); + return ret.getTime(); + } } From a7e50a2b49c6086d8a395b6e50f279686058924f Mon Sep 17 00:00:00 2001 From: Dejan Date: Thu, 6 Oct 2022 16:40:11 +0200 Subject: [PATCH 11/50] Added missing java file --- .gitignore | 1 + build.sh | 9 ++++---- .../com/wire/bots/recording/model/Log.java | 23 +++++++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/wire/bots/recording/model/Log.java diff --git a/.gitignore b/.gitignore index 74f2f3f..c98139d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ Recording Test.html recording/ data/ target/ +libs/ \ No newline at end of file diff --git a/build.sh b/build.sh index 85ba3c7..fee181c 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash -docker build -t $DOCKER_USERNAME/recording-bot:1.0.0 . -docker push $DOCKER_USERNAME/recording-bot -kubectl delete pod -l name=recording -n prod -kubectl get pods -l name=recording -n prod +TAG=1.0.1 +NAME=recording-bot + +docker build -t $DOCKER_USERNAME/$NAME:$TAG . +docker push $DOCKER_USERNAME/$NAME:$TAG diff --git a/src/main/java/com/wire/bots/recording/model/Log.java b/src/main/java/com/wire/bots/recording/model/Log.java new file mode 100644 index 0000000..dfafd77 --- /dev/null +++ b/src/main/java/com/wire/bots/recording/model/Log.java @@ -0,0 +1,23 @@ +package com.wire.bots.recording.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + + +public class Log { + public Kibana securehold; + + public static class Kibana { + public UUID id; + public String type; + public UUID conversationID; + public String conversationName; + public List participants = new ArrayList<>(); + public Long sent; + public String sender; + public UUID messageID; + public String text; + public UUID from; + } +} \ No newline at end of file From a8abfd565b2d4655a11d92594ea1267e7b684327 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Oct 2022 15:31:08 +0200 Subject: [PATCH 12/50] Bump lithium from 3.4.0 to 3.4.2 (#26) Bumps [lithium](https://github.com/wireapp/lithium) from 3.4.0 to 3.4.2. - [Release notes](https://github.com/wireapp/lithium/releases) - [Commits](https://github.com/wireapp/lithium/compare/3.4.0...3.4.2) --- updated-dependencies: - dependency-name: com.wire:lithium dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bea292d..7ef9e4b 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ UTF-8 2.1.2 - 3.4.0 + 3.4.2 1.0.10 0.16.0 0.17.0 From 2312273409cae83c6d9464cbf5c004587f0023ff Mon Sep 17 00:00:00 2001 From: Dejan Kovacevic Date: Tue, 11 Oct 2022 21:43:00 +0200 Subject: [PATCH 13/50] Update docker-compose.yml --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4ba6c68..7a518ec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,7 @@ services: - DB_URL=jdbc:postgresql://db:5432/recording - DB_USER=recording - DB_PASSWORD=recording + - APPENDER_TYPE=json depends_on: - db - elasticsearch @@ -66,4 +67,4 @@ services: - /var/run/docker.sock:/var/run/docker.sock volumes: - recording-db: \ No newline at end of file + recording-db: From 235d2e987c68a9c6e674883e2847810bc786c815 Mon Sep 17 00:00:00 2001 From: Dejan Kovacevic Date: Tue, 11 Oct 2022 21:54:30 +0200 Subject: [PATCH 14/50] Update docker-compose.yml --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7a518ec..062c7e3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: - DB_URL=jdbc:postgresql://db:5432/recording - DB_USER=recording - DB_PASSWORD=recording - - APPENDER_TYPE=json + - APPENDER_TYPE=json-console depends_on: - db - elasticsearch From 7cbaa8a321a43aaa34e63773e5bfbd052cec680f Mon Sep 17 00:00:00 2001 From: Lukas Forst Date: Sat, 15 Oct 2022 12:08:59 +0200 Subject: [PATCH 15/50] add deployment pipelines --- .github/workflows/ci.yml | 30 ++++++++++ .github/workflows/prod.yml | 117 +++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/prod.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3ce10af --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches-ignore: + - master + - staging + + pull_request: + +jobs: + docker-build: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + # setup docker actions https://github.com/docker/build-push-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Build image + id: docker_build + uses: docker/build-push-action@v2 + with: + # https://github.com/docker/build-push-action/issues/220 + context: . + tags: wire/ci-test-image + push: false \ No newline at end of file diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml new file mode 100644 index 0000000..e977300 --- /dev/null +++ b/.github/workflows/prod.yml @@ -0,0 +1,117 @@ +name: Release Pipeline + +on: + release: + types: published + +env: + # set docker image for the service - i.e. "wire-bot/poll" + DOCKER_IMAGE: wire-bot/recording-bot + # name of the service in the Dagobah - the value for label name, i.e. "polls" + SERVICE_NAME: recording-bot + +jobs: + deploy: + name: Build and deploy service + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: Set Release Version + # use latest tag as release version + run: echo "RELEASE_VERSION=${GITHUB_REF:10}" >> $GITHUB_ENV + + # extract metadata for labels https://github.com/crazy-max/ghaction-docker-meta + - name: Docker meta + id: docker_meta + uses: crazy-max/ghaction-docker-meta@v1 + with: + images: eu.gcr.io/${{ env.DOCKER_IMAGE }} + + # setup docker actions https://github.com/docker/build-push-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + # login to GCR repo + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + registry: eu.gcr.io + username: _json_key + password: ${{ secrets.GCR_ACCESS_JSON }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} + # push only if this is indeed a taged release + push: ${{ startsWith(github.ref, 'refs/tags/') }} + build-args: | + release_version=${{ env.RELEASE_VERSION }} + # Checkout our Kubernetes configuration + - name: Checkout Rubicon + uses: actions/checkout@v2 + with: + repository: zinfra/rubicon + # currently main branch is develop + ref: develop + path: rubicon + # private repo so use different git token + token: ${{ secrets.RUBICON_GIT_TOKEN }} + + # Update version to the one that was just built + - name: Change Version in Rubicon + env: + IMAGE: ${{ env.DOCKER_IMAGE }} + SERVICE: ${{ env.SERVICE_NAME }} + VERSION: ${{ env.RELEASE_VERSION }} + run: | + # go to directory with configuration + cd "rubicon/prod/services/$SERVICE" + # escape literals for the sed and set output with GCR + export SED_PREPARED=$(echo $IMAGE | awk '{ gsub("/", "\\/", $1); print "eu.gcr.io\\/"$1 }') + # update final yaml + sed -i".bak" "s/image: $SED_PREPARED.*/image: $SED_PREPARED:$VERSION/g" "$SERVICE.yaml" + # delete bakup file + rm "$SERVICE.yaml.bak" + # Setup gcloud CLI + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v0 + with: + service_account_email: kubernetes-deployment-agent@wire-bot.iam.gserviceaccount.com + service_account_key: ${{ secrets.GKE_SA_KEY }} + project_id: wire-bot + export_default_credentials: true + + # Configure Docker to use the gcloud command-line tool + - name: Configure Docker Google cloud + run: | + gcloud --quiet auth configure-docker + # Get the GKE credentials so we can deploy to the cluster + - name: Obtain k8s credentials + env: + GKE_CLUSTER: anayotto + GKE_ZONE: europe-west1-c + run: | + gcloud container clusters get-credentials "$GKE_CLUSTER" --zone "$GKE_ZONE" + # K8s is set up, deploy the app + - name: Deploy the Service + env: + SERVICE: ${{ env.SERVICE_NAME }} + run: | + kubectl apply -f "rubicon/prod/services/$SERVICE/$SERVICE.yaml" + # Commit all data to Rubicon and open PR + - name: Create Rubicon Pull Request + uses: peter-evans/create-pull-request@v3 + with: + path: rubicon + branch: ${{ env.SERVICE_NAME }}-release + token: ${{ secrets.RUBICON_GIT_TOKEN }} + labels: version-bump, automerge + title: ${{ env.SERVICE_NAME }} release ${{ env.RELEASE_VERSION }} + commit-message: ${{ env.SERVICE_NAME }} version bump to ${{ env.RELEASE_VERSION }} + body: | + This is automatic version bump from the pipeline. \ No newline at end of file From 9e114afea218845851597322bd76ca201a608b15 Mon Sep 17 00:00:00 2001 From: Lukas Forst Date: Sat, 15 Oct 2022 12:13:00 +0200 Subject: [PATCH 16/50] fix service name for the deployment --- .github/workflows/prod.yml | 2 +- .github/workflows/staging.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index e977300..9bd5f53 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -8,7 +8,7 @@ env: # set docker image for the service - i.e. "wire-bot/poll" DOCKER_IMAGE: wire-bot/recording-bot # name of the service in the Dagobah - the value for label name, i.e. "polls" - SERVICE_NAME: recording-bot + SERVICE_NAME: recording jobs: deploy: diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 138898f..ec83283 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -9,7 +9,7 @@ env: # set docker image for the service - i.e. "wire-bot/poll" DOCKER_IMAGE: wire-bot/recording-bot # name of the service in the Dagobah - the value for label name, i.e. "polls" - SERVICE_NAME: recording-bot + SERVICE_NAME: recording jobs: publish: From 5b4291d0fef20f926771f6984b053df0d10a72c7 Mon Sep 17 00:00:00 2001 From: Dejan Date: Wed, 26 Oct 2022 14:51:26 +0200 Subject: [PATCH 17/50] Allow only bot owners to run commands --- .../java/com/wire/bots/recording/MessageHandler.java | 11 ++++++++++- src/main/java/com/wire/bots/recording/Service.java | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index 4358425..632e5d9 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -15,8 +15,10 @@ import com.wire.xenon.assets.MessageText; import com.wire.xenon.backend.models.Conversation; import com.wire.xenon.backend.models.Member; +import com.wire.xenon.backend.models.NewBot; import com.wire.xenon.backend.models.SystemMessage; import com.wire.xenon.exceptions.HttpException; +import com.wire.xenon.factories.StorageFactory; import com.wire.xenon.models.*; import com.wire.xenon.tools.Logger; import com.wire.xenon.tools.Util; @@ -41,13 +43,15 @@ public class MessageHandler extends MessageHandlerBase { "`/private` - stop publishing this conversation"; private final ChannelsDAO channelsDAO; + private final StorageFactory storageFactory; private final EventsDAO eventsDAO; private final EventProcessor eventProcessor = new EventProcessor(); - MessageHandler(EventsDAO eventsDAO, ChannelsDAO channelsDAO) { + MessageHandler(EventsDAO eventsDAO, ChannelsDAO channelsDAO, StorageFactory storageFactory) { this.eventsDAO = eventsDAO; this.channelsDAO = channelsDAO; + this.storageFactory = storageFactory; } void warmup(ClientRepo repo) { @@ -340,6 +344,11 @@ private void generateHtml(WireClient client, UUID botId, UUID convId) { } private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, String cmd) throws Exception { + // Only owner of the bot can run commands + NewBot state = storageFactory.create(client.getId()).getState(); + if(state.origin.id != userId) + return false; + switch (cmd) { case "/help": { client.send(new MessageText(HELP), userId); diff --git a/src/main/java/com/wire/bots/recording/Service.java b/src/main/java/com/wire/bots/recording/Service.java index fa707db..43bbc52 100644 --- a/src/main/java/com/wire/bots/recording/Service.java +++ b/src/main/java/com/wire/bots/recording/Service.java @@ -60,7 +60,7 @@ protected MessageHandlerBase createHandler(Config config, Environment env) { final EventsDAO eventsDAO = jdbi.onDemand(EventsDAO.class); final ChannelsDAO channelsDAO = jdbi.onDemand(ChannelsDAO.class); - messageHandler = new MessageHandler(eventsDAO, channelsDAO); + messageHandler = new MessageHandler(eventsDAO, channelsDAO, getStorageFactory()); return messageHandler; } From fc5902406429fbf2735d061c7f64182474ada939 Mon Sep 17 00:00:00 2001 From: Dejan Date: Wed, 26 Oct 2022 15:48:01 +0200 Subject: [PATCH 18/50] logs --- src/main/java/com/wire/bots/recording/MessageHandler.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index 632e5d9..16e6cb6 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -345,8 +345,11 @@ private void generateHtml(WireClient client, UUID botId, UUID convId) { private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, String cmd) throws Exception { // Only owner of the bot can run commands - NewBot state = storageFactory.create(client.getId()).getState(); - if(state.origin.id != userId) + NewBot state = storageFactory.create(botId).getState(); + + Logger.info("Command: '%s', user: %s, origin: %s", cmd, userId, state.origin.id); + + if (state.origin.id != userId) return false; switch (cmd) { From 3a837567ea46280f76c462fadd5f85beaa06eb99 Mon Sep 17 00:00:00 2001 From: Dejan Date: Wed, 26 Oct 2022 17:53:23 +0200 Subject: [PATCH 19/50] less logs --- src/main/java/com/wire/bots/recording/MessageHandler.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index 16e6cb6..89c04c1 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -29,6 +29,7 @@ import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.List; +import java.util.Objects; import java.util.UUID; import static com.wire.bots.recording.utils.Helper.date; @@ -346,10 +347,7 @@ private void generateHtml(WireClient client, UUID botId, UUID convId) { private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, String cmd) throws Exception { // Only owner of the bot can run commands NewBot state = storageFactory.create(botId).getState(); - - Logger.info("Command: '%s', user: %s, origin: %s", cmd, userId, state.origin.id); - - if (state.origin.id != userId) + if (!Objects.equals(state.origin.id, userId)) return false; switch (cmd) { From 7ff0a0274de36aec3500c37aea13bcfcd92fc11d Mon Sep 17 00:00:00 2001 From: Dejan Date: Wed, 26 Oct 2022 18:27:23 +0200 Subject: [PATCH 20/50] override readResource in ImagesBundle --- .travis.yml | 16 ------------- .../bots/recording/utils/ImagesBundle.java | 24 +++++++++++++++++-- 2 files changed, 22 insertions(+), 18 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 098f81b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -sudo: 'required' - -language: java - -notifications: - email: false - -services: -- docker - -after_success: - - if [[ "$TRAVIS_BRANCH" == "master" ]]; then - docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD ; - docker build -t $DOCKER_USERNAME/recording-bot:latest . ; - docker push $DOCKER_USERNAME/recording-bot ; - fi diff --git a/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java b/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java index 199135a..0777246 100644 --- a/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java +++ b/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java @@ -3,8 +3,13 @@ import com.wire.xenon.tools.Logger; import io.dropwizard.assets.AssetsBundle; import io.dropwizard.servlets.assets.AssetServlet; +import io.dropwizard.util.ByteStreams; import javax.annotation.Nullable; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; @@ -26,14 +31,29 @@ static class _AssetServlet extends AssetServlet { } @Override - protected URL getResourceUrl(String path) { - Logger.debug("ImagesBundle: loading: %s", path); + protected URL getResourceURL(String path) { + Logger.info("ImagesBundle: loading: %s", path); try { + File file = new File(path); + if (!file.exists()) { + Logger.warning("ImagesBundle: file does not exist: %s", path); + return null; + } return new URL(String.format("file:/%s", path)); } catch (MalformedURLException e) { //Logger.error(e.toString()); return null; } } + + @Override + protected byte[] readResource(URL requestedResourceURL) throws IOException { + try (InputStream inputStream = requestedResourceURL.openStream()) { + return ByteStreams.toByteArray(inputStream); + } catch (FileNotFoundException e) { + Logger.warning("ImagesBundle: %s", e); + return null; + } + } } } From 1e8788158194eff6e105c7c56e9e16d69c696255 Mon Sep 17 00:00:00 2001 From: Dejan Date: Wed, 26 Oct 2022 19:36:10 +0200 Subject: [PATCH 21/50] override readResource in ImagesBundle --- .../com/wire/bots/recording/utils/ImagesBundle.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java b/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java index 0777246..304c117 100644 --- a/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java +++ b/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java @@ -1,9 +1,9 @@ package com.wire.bots.recording.utils; import com.wire.xenon.tools.Logger; +import com.wire.xenon.tools.Util; import io.dropwizard.assets.AssetsBundle; import io.dropwizard.servlets.assets.AssetServlet; -import io.dropwizard.util.ByteStreams; import javax.annotation.Nullable; import java.io.File; @@ -34,12 +34,14 @@ static class _AssetServlet extends AssetServlet { protected URL getResourceURL(String path) { Logger.info("ImagesBundle: loading: %s", path); try { - File file = new File(path); + String format = String.format("file:/%s", path); + + File file = new File(format); if (!file.exists()) { - Logger.warning("ImagesBundle: file does not exist: %s", path); + Logger.warning("ImagesBundle: file does not exist: %s", format); return null; } - return new URL(String.format("file:/%s", path)); + return new URL(format); } catch (MalformedURLException e) { //Logger.error(e.toString()); return null; @@ -49,7 +51,7 @@ protected URL getResourceURL(String path) { @Override protected byte[] readResource(URL requestedResourceURL) throws IOException { try (InputStream inputStream = requestedResourceURL.openStream()) { - return ByteStreams.toByteArray(inputStream); + return Util.toByteArray(inputStream); } catch (FileNotFoundException e) { Logger.warning("ImagesBundle: %s", e); return null; From 7fb4c1431b2324078c43201ee51af788f1804986 Mon Sep 17 00:00:00 2001 From: Dejan Date: Wed, 26 Oct 2022 19:47:32 +0200 Subject: [PATCH 22/50] override readResource in ImagesBundle --- src/main/java/com/wire/bots/recording/utils/ImagesBundle.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java b/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java index 304c117..caf2811 100644 --- a/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java +++ b/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java @@ -36,9 +36,9 @@ protected URL getResourceURL(String path) { try { String format = String.format("file:/%s", path); - File file = new File(format); + File file = new File("/" + path); if (!file.exists()) { - Logger.warning("ImagesBundle: file does not exist: %s", format); + Logger.warning("ImagesBundle: file does not exist: /%s", path); return null; } return new URL(format); From 536568d8b081957be51eb89b00c88b0ce79df442 Mon Sep 17 00:00:00 2001 From: Dejan Date: Thu, 27 Oct 2022 11:23:56 +0200 Subject: [PATCH 23/50] Logging for download assets --- pom.xml | 21 ++++++++++++++++++- .../wire/bots/recording/MessageHandler.java | 9 ++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7ef9e4b..cf3e88c 100644 --- a/pom.xml +++ b/pom.xml @@ -26,13 +26,14 @@ UTF-8 UTF-8 - 2.1.2 + 2.1.4 3.4.2 1.0.10 0.16.0 0.17.0 0.9.10 2.11.1 + 5.9.0 true @@ -43,6 +44,24 @@ lithium ${lithium.version} + + io.dropwizard + dropwizard-testing + ${dropwizard.version} + test + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + com.github.spullara.mustache.java compiler diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index 89c04c1..598706c 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -77,6 +77,12 @@ void warmup(ClientRepo repo) { Logger.info("Finished Warming up %d convs", conversations.size()); } + @Override + public boolean onNewBot(NewBot newBot, String serviceToken) { + Logger.info("New bot: conv: %s, token: %s", newBot.conversation.id, newBot.token); + return true; + } + @Override public void onNewConversation(WireClient client, SystemMessage msg) { try { @@ -226,6 +232,9 @@ public void onAssetData(WireClient client, RemoteMessage msg) { String type = "conversation.otr-message-add.asset-data"; try { + String payload = mapper.writeValueAsString(msg); + Logger.info("Persisting: '%s'", payload); + persist(convId, userId, botId, messageId, type, msg); } catch (Exception e) { Logger.error("onAssetData: %s %s %s", botId, messageId, e); From a9dc8bf12fe0a0b97c939a1666753dd368c97acc Mon Sep 17 00:00:00 2001 From: Dejan Date: Thu, 27 Oct 2022 12:28:31 +0200 Subject: [PATCH 24/50] Lithium 3.4.3 --- pom.xml | 2 +- .../wire/bots/recording/MessageHandler.java | 9 --- .../com/wire/bots/recording/AssetTests.java | 78 +++++++++++++++++++ 3 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 src/test/java/com/wire/bots/recording/AssetTests.java diff --git a/pom.xml b/pom.xml index cf3e88c..095dc5a 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ UTF-8 2.1.4 - 3.4.2 + 3.4.3 1.0.10 0.16.0 0.17.0 diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index 598706c..89c04c1 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -77,12 +77,6 @@ void warmup(ClientRepo repo) { Logger.info("Finished Warming up %d convs", conversations.size()); } - @Override - public boolean onNewBot(NewBot newBot, String serviceToken) { - Logger.info("New bot: conv: %s, token: %s", newBot.conversation.id, newBot.token); - return true; - } - @Override public void onNewConversation(WireClient client, SystemMessage msg) { try { @@ -232,9 +226,6 @@ public void onAssetData(WireClient client, RemoteMessage msg) { String type = "conversation.otr-message-add.asset-data"; try { - String payload = mapper.writeValueAsString(msg); - Logger.info("Persisting: '%s'", payload); - persist(convId, userId, botId, messageId, type, msg); } catch (Exception e) { Logger.error("onAssetData: %s %s %s", botId, messageId, e); diff --git a/src/test/java/com/wire/bots/recording/AssetTests.java b/src/test/java/com/wire/bots/recording/AssetTests.java new file mode 100644 index 0000000..fc4ee7a --- /dev/null +++ b/src/test/java/com/wire/bots/recording/AssetTests.java @@ -0,0 +1,78 @@ +package com.wire.bots.recording; + +import com.ning.http.util.Base64; +import com.wire.lithium.API; +import com.wire.xenon.tools.Util; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.DropwizardTestSupport; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.ws.rs.client.Client; +import java.security.MessageDigest; +import java.util.Arrays; + +public class AssetTests { + + private DropwizardTestSupport support; + + private Client client; + + @Before + public void setup() throws Exception { + + support = new DropwizardTestSupport<>( + Service.class, + "recording.yaml", + ConfigOverride.config("token", "dummy"), + ConfigOverride.config("healthchecks", "false") + ); + + support.before(); + + final Service server = (Service) support.getApplication(); + client = server.getClient(); + } + + @After + public void after() { + support.after(); + } + + @Test + public void downloadAssetTest() throws Exception { + /* + ``` + { + "eventId":"2be2efec-5a59-4f56-8d2f-4155a73c4415", + "messageId":"ed10eb10-c429-4387-89f7-ab27b1b25adf", + "conversationId":"6519f09f-8c72-4397-9f6e-cd7da4ed7e78", + "clientId":"1a955d5af444a8e3", + "userId":"0a2203f9-b0c2-4dfe-a349-f668dfd1397b", + "time":"2022-10-27T09:31:06.974Z", + "assetId":"3-4-22d347d5-4e74-44f7-bf5f-d73838bffd79", + "assetToken":"", + "otrKey":"iRnwKWIhs/NjMPefUalDXYQuCJ24Cx/7GFrKYYWfrEU=", + "sha256":"unFvlwnskg0I6pBzIVdF6844gS/EjjxoaAjRhRKXs6w=" + } + ``` + */ + + String botToken = "oRypYNXLKSzCzZRFLlupFnbnvjR_OuCIE7gW55b-nHruEqdecAKn9VW69VnNojaEIMB4fpVjQgCSib3nca52Aw==.v=1.k=1.d=-1.t=b.l=.p=d64af9ae-e0c5-4ce6-b38a-02fd9363b54c.b=8764b27e-ad87-433f-a6d9-6c41c20b9f81.c=6519f09f-8c72-4397-9f6e-cd7da4ed7e78"; + String assetId = "3-4-22d347d5-4e74-44f7-bf5f-d73838bffd79"; + String assetToken = ""; + byte[] otrKey = Base64.decode("iRnwKWIhs/NjMPefUalDXYQuCJ24Cx/7GFrKYYWfrEU="); + byte[] sha256Challenge = Base64.decode("unFvlwnskg0I6pBzIVdF6844gS/EjjxoaAjRhRKXs6w="); + + API api = new API(client, botToken, "https://staging-nginz-https.zinfra.io"); + + byte[] cipher = api.downloadAsset(assetId, assetToken); + byte[] sha256 = MessageDigest.getInstance("SHA-256").digest(cipher); + + if (!Arrays.equals(sha256, sha256Challenge)) + throw new Exception("Failed sha256 check"); + + byte[] asset = Util.decrypt(otrKey, cipher); + } +} From 7dae6b44d566db4cb5d92b86b068ae263ce55104 Mon Sep 17 00:00:00 2001 From: Dejan Date: Thu, 27 Oct 2022 12:45:35 +0200 Subject: [PATCH 25/50] FileAssets --- src/main/java/com/wire/bots/recording/MessageHandler.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index 89c04c1..e1b950e 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -24,6 +24,7 @@ import com.wire.xenon.tools.Util; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -370,7 +371,9 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, UUID messageId = UUID.randomUUID(); String mimeType = "application/pdf"; client.send(new FileAssetPreview(pdfFile.getName(), mimeType, pdfFile.length(), messageId), userId); - client.send(new FileAsset(messageId, mimeType), userId); + + byte[] bytes = Util.toByteArray(new FileInputStream(pdfFile)); + client.send(new FileAsset(bytes, mimeType, messageId), userId); return true; } case "/public": { From 98f0b546375362d8c95e8f5dd2cbaed80d54029d Mon Sep 17 00:00:00 2001 From: Dejan Date: Thu, 27 Oct 2022 12:56:06 +0200 Subject: [PATCH 26/50] FileAssets uploads --- .../java/com/wire/bots/recording/MessageHandler.java | 11 ++++++++++- .../com/wire/bots/recording/utils/ImagesBundle.java | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index e1b950e..6c33c73 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -368,12 +368,21 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, String pdfFilename = String.format("html/%s.pdf", URLEncoder.encode(convName, StandardCharsets.UTF_8)); File pdfFile = PdfGenerator.save(pdfFilename, html, "file:/opt"); + // Post the Preview UUID messageId = UUID.randomUUID(); String mimeType = "application/pdf"; client.send(new FileAssetPreview(pdfFile.getName(), mimeType, pdfFile.length(), messageId), userId); byte[] bytes = Util.toByteArray(new FileInputStream(pdfFile)); - client.send(new FileAsset(bytes, mimeType, messageId), userId); + FileAsset fileAsset = new FileAsset(bytes, mimeType, messageId); + + // Upload Asset + AssetKey assetKey = client.uploadAsset(fileAsset); + fileAsset.setAssetToken(assetKey.token); + fileAsset.setAssetKey(assetKey.id); + + // Post Asset + client.send(fileAsset, userId); return true; } case "/public": { diff --git a/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java b/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java index caf2811..373e515 100644 --- a/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java +++ b/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java @@ -32,7 +32,7 @@ static class _AssetServlet extends AssetServlet { @Override protected URL getResourceURL(String path) { - Logger.info("ImagesBundle: loading: %s", path); + Logger.debug("ImagesBundle: loading: %s", path); try { String format = String.format("file:/%s", path); From 5288ffd4a1f6b960afc580cad9b435e452aed874 Mon Sep 17 00:00:00 2001 From: Dejan Date: Thu, 27 Oct 2022 16:22:45 +0200 Subject: [PATCH 27/50] more logs --- src/main/java/com/wire/bots/recording/MessageHandler.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index 6c33c73..f4b9454 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -373,13 +373,14 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, String mimeType = "application/pdf"; client.send(new FileAssetPreview(pdfFile.getName(), mimeType, pdfFile.length(), messageId), userId); - byte[] bytes = Util.toByteArray(new FileInputStream(pdfFile)); - FileAsset fileAsset = new FileAsset(bytes, mimeType, messageId); + FileAsset fileAsset = new FileAsset(pdfFile, mimeType, messageId); // Upload Asset AssetKey assetKey = client.uploadAsset(fileAsset); - fileAsset.setAssetToken(assetKey.token); fileAsset.setAssetKey(assetKey.id); + fileAsset.setAssetToken(assetKey.token); + + Logger.info("Uploaded assetID: %s, token: %s", assetKey.id, assetKey.token); // Post Asset client.send(fileAsset, userId); From 12cd900c49a9b0d59ffe74e77d1633ba0ec58070 Mon Sep 17 00:00:00 2001 From: Dejan Date: Thu, 27 Oct 2022 17:20:12 +0200 Subject: [PATCH 28/50] Lithium 3.4.4 with fixed AssetKey class --- pom.xml | 4 ++-- .../com/wire/bots/recording/MessageHandler.java | 2 -- .../com/wire/bots/recording/AssetTests.java | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 095dc5a..7c3b341 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ recording-bot com.wire.bots - 0.3.0 + 0.3.2 Recording Bot Recording Bot Service For Wire @@ -27,7 +27,7 @@ UTF-8 2.1.4 - 3.4.3 + 3.4.4 1.0.10 0.16.0 0.17.0 diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index f4b9454..fbe3d5a 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -380,8 +380,6 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, fileAsset.setAssetKey(assetKey.id); fileAsset.setAssetToken(assetKey.token); - Logger.info("Uploaded assetID: %s, token: %s", assetKey.id, assetKey.token); - // Post Asset client.send(fileAsset, userId); return true; diff --git a/src/test/java/com/wire/bots/recording/AssetTests.java b/src/test/java/com/wire/bots/recording/AssetTests.java index fc4ee7a..3123f98 100644 --- a/src/test/java/com/wire/bots/recording/AssetTests.java +++ b/src/test/java/com/wire/bots/recording/AssetTests.java @@ -2,6 +2,8 @@ import com.ning.http.util.Base64; import com.wire.lithium.API; +import com.wire.xenon.assets.FileAsset; +import com.wire.xenon.models.AssetKey; import com.wire.xenon.tools.Util; import io.dropwizard.testing.ConfigOverride; import io.dropwizard.testing.DropwizardTestSupport; @@ -10,8 +12,10 @@ import org.junit.Test; import javax.ws.rs.client.Client; +import java.io.File; import java.security.MessageDigest; import java.util.Arrays; +import java.util.UUID; public class AssetTests { @@ -75,4 +79,17 @@ public void downloadAssetTest() throws Exception { byte[] asset = Util.decrypt(otrKey, cipher); } + + @Test + public void uploadAssetTest() throws Exception { + String botToken = "oRypYNXLKSzCzZRFLlupFnbnvjR_OuCIE7gW55b-nHruEqdecAKn9VW69VnNojaEIMB4fpVjQgCSib3nca52Aw==.v=1.k=1.d=-1.t=b.l=.p=d64af9ae-e0c5-4ce6-b38a-02fd9363b54c.b=8764b27e-ad87-433f-a6d9-6c41c20b9f81.c=6519f09f-8c72-4397-9f6e-cd7da4ed7e78"; + API api = new API(client, botToken, "https://staging-nginz-https.zinfra.io"); + + File file = new File("recording.yaml"); + FileAsset fileAsset = new FileAsset(file, "image/jpeg", UUID.randomUUID()); + + //{"domain":"staging.zinfra.io","key":"3-2-726da28c-7fa9-4be4-b818-d0939ce7b4a2","token":"3s7abpMBb9YcqhbRqU64fA=="} + AssetKey assetKey = api.uploadAsset(fileAsset); + + } } From fd3c26b640126769e4f87f897b360592510f31b1 Mon Sep 17 00:00:00 2001 From: Dejan Date: Fri, 28 Oct 2022 11:14:27 +0200 Subject: [PATCH 29/50] Hash asset keys --- pom.xml | 7 +++- .../com/wire/bots/recording/utils/Cache.java | 32 ++++++++++++++----- .../wire/bots/recording/utils/Collector.java | 14 ++++---- .../com/wire/bots/recording/utils/Helper.java | 11 +++---- .../wire/bots/recording/utils/TestCache.java | 5 +++ 5 files changed, 48 insertions(+), 21 deletions(-) diff --git a/pom.xml b/pom.xml index 7c3b341..dee5e56 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ 0.16.0 0.17.0 0.9.10 - 2.11.1 + 2.11.2 5.9.0 true @@ -103,6 +103,11 @@ 4.13.2 test + + com.lambdaworks + scrypt + 1.4.0 + diff --git a/src/main/java/com/wire/bots/recording/utils/Cache.java b/src/main/java/com/wire/bots/recording/utils/Cache.java index 0585fae..5e4538e 100644 --- a/src/main/java/com/wire/bots/recording/utils/Cache.java +++ b/src/main/java/com/wire/bots/recording/utils/Cache.java @@ -1,9 +1,11 @@ package com.wire.bots.recording.utils; +import com.lambdaworks.crypto.SCryptUtil; import com.wire.xenon.WireClient; import com.wire.xenon.backend.models.User; import com.wire.xenon.exceptions.HttpException; import com.wire.xenon.models.RemoteMessage; +import com.wire.xenon.tools.Util; import java.io.File; import java.util.UUID; @@ -23,21 +25,33 @@ public static void clear(UUID userId) { } File getAssetFile(RemoteMessage message) { - return assetsMap.computeIfAbsent(message.getAssetId(), k -> { + String key = key(message.getAssetId()); + return assetsMap.computeIfAbsent(key, k -> { try { - byte[] image = downloadAsset(message); - return Helper.saveAsset(image, message); + byte[] image = client.downloadAsset(message.getAssetId(), + message.getAssetToken(), + message.getSha256(), + message.getOtrKey()); + return Helper.saveAsset(image, key); } catch (Exception e) { throw new RuntimeException(e); } }); } - protected byte[] downloadAsset(RemoteMessage message) throws Exception { - return client.downloadAsset(message.getAssetId(), - message.getAssetToken(), - message.getSha256(), - message.getOtrKey()); + private String key(String assetId) { + return SCryptUtil.scrypt(assetId, 16384, 8, 1); + } + + public File getProfileFile(String key) { + return assetsMap.computeIfAbsent(key, k -> { + try { + byte[] image = client.downloadProfilePicture(key); + return Helper.saveProfileAsset(image, k); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); } protected User getUserInternal(UUID userId) throws HttpException { @@ -53,4 +67,6 @@ public User getUser(UUID userId) { } }); } + + } diff --git a/src/main/java/com/wire/bots/recording/utils/Collector.java b/src/main/java/com/wire/bots/recording/utils/Collector.java index 0f6f372..b2ee55c 100644 --- a/src/main/java/com/wire/bots/recording/utils/Collector.java +++ b/src/main/java/com/wire/bots/recording/utils/Collector.java @@ -6,6 +6,7 @@ import com.wire.xenon.backend.models.Asset; import com.wire.xenon.backend.models.User; import com.wire.xenon.models.*; +import com.wire.xenon.tools.Logger; import javax.annotation.Nullable; import java.io.*; @@ -307,12 +308,13 @@ private String getFilename(File file) { @Nullable private String getAvatar(UUID userId) { -// User user = cache.getProfile(userId); -// String profileAssetKey = getProfileAssetKey(user.assets); -// if (profileAssetKey != null) { -// File file = cache.getProfileImage(profileAssetKey); -// return String.format("/%s/%s", "avatars", file.getName()); -// } + User user = cache.getUser(userId); + String profileAssetKey = getProfileAssetKey(user.assets); + if (profileAssetKey != null) { + File file = cache.getProfileFile(profileAssetKey); + return String.format("/%s/%s", "avatars", file.getName()); + } + Logger.warning("User %s has no profile picture", userId); return null; } diff --git a/src/main/java/com/wire/bots/recording/utils/Helper.java b/src/main/java/com/wire/bots/recording/utils/Helper.java index 9c565fd..9c711a0 100644 --- a/src/main/java/com/wire/bots/recording/utils/Helper.java +++ b/src/main/java/com/wire/bots/recording/utils/Helper.java @@ -1,6 +1,5 @@ package com.wire.bots.recording.utils; -import com.wire.xenon.models.RemoteMessage; import com.wire.xenon.tools.Logger; import org.commonmark.Extension; import org.commonmark.ext.autolink.AutolinkExtension; @@ -27,16 +26,16 @@ public class Helper { .extensions(extensions) .build(); - static File getProfile(byte[] profile, String key) throws Exception { + static File saveProfileAsset(byte[] image, String key) throws Exception { String filename = avatarFile(key); File file = new File(filename); - Logger.info("downloaded profile: %s, size: %d, file: %s", key, profile.length, file.getAbsolutePath()); - return save(profile, file); + Logger.info("downloaded profile: %s, size: %d, file: %s", key, image.length, file.getAbsolutePath()); + return save(image, file); } - static File saveAsset(byte[] image, RemoteMessage message) throws Exception { - File file = assetFile(message.getAssetId(), "image/jpeg"); + static File saveAsset(byte[] image, String key) throws Exception { + File file = assetFile(key, "image/jpeg"); return save(image, file); } diff --git a/src/test/java/com/wire/bots/recording/utils/TestCache.java b/src/test/java/com/wire/bots/recording/utils/TestCache.java index b0ed7fb..54bf698 100644 --- a/src/test/java/com/wire/bots/recording/utils/TestCache.java +++ b/src/test/java/com/wire/bots/recording/utils/TestCache.java @@ -45,4 +45,9 @@ public User getUser(UUID userId) { File getAssetFile(RemoteMessage message) { return new File(String.format("src/test/resources/avatars/%s.png", message.getAssetId())); } + + @Override + public File getProfileFile(String key) { + return new File(String.format("src/test/resources/avatars/%s.png",key)); + } } From 55cadd229d21e0290a6e989395fd494c6cb25103 Mon Sep 17 00:00:00 2001 From: Dejan Date: Fri, 28 Oct 2022 18:51:55 +0200 Subject: [PATCH 30/50] SHA 256 Hash asset keys --- pom.xml | 5 ----- .../com/wire/bots/recording/utils/Cache.java | 19 +++++++++++-------- .../wire/bots/recording/utils/Collector.java | 6 +++--- .../com/wire/bots/recording/AssetTests.java | 11 +++++++++++ 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index dee5e56..86a0e6b 100644 --- a/pom.xml +++ b/pom.xml @@ -103,11 +103,6 @@ 4.13.2 test - - com.lambdaworks - scrypt - 1.4.0 - diff --git a/src/main/java/com/wire/bots/recording/utils/Cache.java b/src/main/java/com/wire/bots/recording/utils/Cache.java index 5e4538e..4e1ed99 100644 --- a/src/main/java/com/wire/bots/recording/utils/Cache.java +++ b/src/main/java/com/wire/bots/recording/utils/Cache.java @@ -1,13 +1,15 @@ package com.wire.bots.recording.utils; -import com.lambdaworks.crypto.SCryptUtil; import com.wire.xenon.WireClient; import com.wire.xenon.backend.models.User; import com.wire.xenon.exceptions.HttpException; import com.wire.xenon.models.RemoteMessage; -import com.wire.xenon.tools.Util; +import org.eclipse.jetty.util.UrlEncoded; import java.io.File; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -24,7 +26,7 @@ public static void clear(UUID userId) { users.remove(userId); } - File getAssetFile(RemoteMessage message) { + File getAssetFile(RemoteMessage message) throws NoSuchAlgorithmException { String key = key(message.getAssetId()); return assetsMap.computeIfAbsent(key, k -> { try { @@ -39,10 +41,6 @@ File getAssetFile(RemoteMessage message) { }); } - private String key(String assetId) { - return SCryptUtil.scrypt(assetId, 16384, 8, 1); - } - public File getProfileFile(String key) { return assetsMap.computeIfAbsent(key, k -> { try { @@ -68,5 +66,10 @@ public User getUser(UUID userId) { }); } - + private String key(String assetId) throws NoSuchAlgorithmException { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + messageDigest.update(assetId.getBytes()); + String encode = Base64.getEncoder().encodeToString(messageDigest.digest()); + return UrlEncoded.encodeString(encode); + } } diff --git a/src/main/java/com/wire/bots/recording/utils/Collector.java b/src/main/java/com/wire/bots/recording/utils/Collector.java index b2ee55c..58853d6 100644 --- a/src/main/java/com/wire/bots/recording/utils/Collector.java +++ b/src/main/java/com/wire/bots/recording/utils/Collector.java @@ -102,7 +102,7 @@ public void addEdit(EditedTextMessage event) throws ParseException { add(event); } - public void add(RemoteMessage event) throws ParseException { + public void add(RemoteMessage event) throws Exception { Message message = new Message(); message.id = event.getMessageId(); message.timeStamp = event.getTime(); @@ -116,7 +116,7 @@ public void add(RemoteMessage event) throws ParseException { append(sender, message, event.getTime()); } - public void add(RemoteMessage event, VideoPreviewMessage preview) throws ParseException { + public void add(RemoteMessage event, VideoPreviewMessage preview) throws Exception { Message message = new Message(); message.id = event.getMessageId(); message.timeStamp = event.getTime(); @@ -134,7 +134,7 @@ public void add(RemoteMessage event, VideoPreviewMessage preview) throws ParseEx append(sender, message, event.getTime()); } - public Sender add(RemoteMessage event, FilePreviewMessage preview) throws ParseException { + public Sender add(RemoteMessage event, FilePreviewMessage preview) throws Exception { Message message = new Message(); message.id = event.getMessageId(); message.timeStamp = event.getTime(); diff --git a/src/test/java/com/wire/bots/recording/AssetTests.java b/src/test/java/com/wire/bots/recording/AssetTests.java index 3123f98..44d7889 100644 --- a/src/test/java/com/wire/bots/recording/AssetTests.java +++ b/src/test/java/com/wire/bots/recording/AssetTests.java @@ -7,6 +7,7 @@ import com.wire.xenon.tools.Util; import io.dropwizard.testing.ConfigOverride; import io.dropwizard.testing.DropwizardTestSupport; +import org.eclipse.jetty.util.UrlEncoded; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -14,6 +15,7 @@ import javax.ws.rs.client.Client; import java.io.File; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.UUID; @@ -92,4 +94,13 @@ public void uploadAssetTest() throws Exception { AssetKey assetKey = api.uploadAsset(fileAsset); } + + @Test + public void hashTest() throws NoSuchAlgorithmException { + String assetId = "3-4-22d347d5-4e74-44f7-bf5f-d73838bffd79"; + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + messageDigest.update(assetId.getBytes()); + String hash = Base64.encode(messageDigest.digest()); + String encode = UrlEncoded.encodeString(hash); + } } From fe33d5cf8366e7aebf96fbfb50deb034c474279a Mon Sep 17 00:00:00 2001 From: Dejan Date: Fri, 28 Oct 2022 21:08:36 +0200 Subject: [PATCH 31/50] obfuscate conversation id --- .../wire/bots/recording/MessageHandler.java | 19 +++++++++++++------ .../com/wire/bots/recording/utils/Cache.java | 15 ++++----------- .../com/wire/bots/recording/utils/Helper.java | 12 ++++++++++++ .../com/wire/bots/recording/AssetTests.java | 7 +++---- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index fbe3d5a..b1c075d 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -6,6 +6,7 @@ import com.wire.bots.recording.DAO.EventsDAO; import com.wire.bots.recording.model.Event; import com.wire.bots.recording.model.Log; +import com.wire.bots.recording.utils.Helper; import com.wire.bots.recording.utils.PdfGenerator; import com.wire.lithium.ClientRepo; import com.wire.xenon.MessageHandlerBase; @@ -24,10 +25,10 @@ import com.wire.xenon.tools.Util; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; import java.text.ParseException; import java.util.List; import java.util.Objects; @@ -64,7 +65,7 @@ void warmup(ClientRepo repo) { UUID botId = channelsDAO.getBotId(convId); if (botId != null) { try (WireClient client = repo.getClient(botId)) { - String filename = String.format("html/%s.html", convId); + String filename = getConversationPath(convId); List events = eventsDAO.listAllAsc(convId); File file = eventProcessor.saveHtml(client, events, filename, false); Logger.debug("warmed up: %s", file.getName()); @@ -335,7 +336,7 @@ private void generateHtml(WireClient client, UUID botId, UUID convId) { try { if (null != channelsDAO.contains(convId)) { List events = eventsDAO.listAllAsc(convId); - String filename = String.format("html/%s.html", convId); + String filename = getConversationPath(convId); File file = eventProcessor.saveHtml(client, events, filename, false); assert file.exists(); @@ -358,7 +359,7 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, } case "/pdf": { client.send(new MessageText("Generating PDF..."), userId); - String filename = String.format("html/%s.html", convId); + String filename = getConversationPath(convId); List events = eventsDAO.listAllAsc(convId); File file = eventProcessor.saveHtml(client, events, filename, true); @@ -386,13 +387,14 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, } case "/public": { channelsDAO.insert(convId, botId); - String text = String.format("%s/channel/%s.html", Service.instance.getConfig().url, convId); + String key = Helper.key(convId.toString()); + String text = String.format("%s/channel/%s.html", Service.instance.getConfig().url, key); client.send(new MessageText(text), userId); return true; } case "/private": { channelsDAO.delete(convId); - String filename = String.format("html/%s.html", convId); + String filename = getConversationPath(convId); boolean delete = new File(filename).delete(); String txt = String.format("%s deleted: %s", filename, delete); client.send(new MessageText(txt), userId); @@ -402,6 +404,11 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, return false; } + private String getConversationPath(UUID convId) throws NoSuchAlgorithmException { + String key = Helper.key(convId.toString()); + return String.format("html/%s.html", key); + } + private void persist(UUID convId, UUID senderId, UUID userId, UUID msgId, String type, Object msg) throws RuntimeException { try { diff --git a/src/main/java/com/wire/bots/recording/utils/Cache.java b/src/main/java/com/wire/bots/recording/utils/Cache.java index 4e1ed99..9340eb3 100644 --- a/src/main/java/com/wire/bots/recording/utils/Cache.java +++ b/src/main/java/com/wire/bots/recording/utils/Cache.java @@ -27,7 +27,7 @@ public static void clear(UUID userId) { } File getAssetFile(RemoteMessage message) throws NoSuchAlgorithmException { - String key = key(message.getAssetId()); + String key = Helper.key(message.getAssetId()); return assetsMap.computeIfAbsent(key, k -> { try { byte[] image = client.downloadAsset(message.getAssetId(), @@ -41,10 +41,10 @@ File getAssetFile(RemoteMessage message) throws NoSuchAlgorithmException { }); } - public File getProfileFile(String key) { - return assetsMap.computeIfAbsent(key, k -> { + public File getProfileFile(String assetId) { + return assetsMap.computeIfAbsent(assetId, k -> { try { - byte[] image = client.downloadProfilePicture(key); + byte[] image = client.downloadProfilePicture(assetId); return Helper.saveProfileAsset(image, k); } catch (Exception e) { throw new RuntimeException(e); @@ -65,11 +65,4 @@ public User getUser(UUID userId) { } }); } - - private String key(String assetId) throws NoSuchAlgorithmException { - MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); - messageDigest.update(assetId.getBytes()); - String encode = Base64.getEncoder().encodeToString(messageDigest.digest()); - return UrlEncoded.encodeString(encode); - } } diff --git a/src/main/java/com/wire/bots/recording/utils/Helper.java b/src/main/java/com/wire/bots/recording/utils/Helper.java index 9c711a0..7cbb183 100644 --- a/src/main/java/com/wire/bots/recording/utils/Helper.java +++ b/src/main/java/com/wire/bots/recording/utils/Helper.java @@ -6,14 +6,18 @@ import org.commonmark.node.Node; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; +import org.eclipse.jetty.util.UrlEncoded; import javax.annotation.Nullable; import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Base64; import java.util.Collections; import java.util.Date; import java.util.List; @@ -43,6 +47,7 @@ private static File save(byte[] image, File file) throws IOException { try (DataOutputStream os = new DataOutputStream(new FileOutputStream(file))) { os.write(image); } + Logger.info("Saved asset: %s", file.getAbsolutePath()); return file; } @@ -81,4 +86,11 @@ public static Long date(@Nullable String date) throws ParseException { Date ret = parser.parse(date); return ret.getTime(); } + + public static String key(String assetId) throws NoSuchAlgorithmException { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + messageDigest.update(assetId.getBytes()); + String encode = Base64.getEncoder().encodeToString(messageDigest.digest()); + return UrlEncoded.encodeString(encode); + } } diff --git a/src/test/java/com/wire/bots/recording/AssetTests.java b/src/test/java/com/wire/bots/recording/AssetTests.java index 44d7889..249bc6d 100644 --- a/src/test/java/com/wire/bots/recording/AssetTests.java +++ b/src/test/java/com/wire/bots/recording/AssetTests.java @@ -1,6 +1,8 @@ package com.wire.bots.recording; import com.ning.http.util.Base64; +import com.wire.bots.recording.utils.Cache; +import com.wire.bots.recording.utils.Helper; import com.wire.lithium.API; import com.wire.xenon.assets.FileAsset; import com.wire.xenon.models.AssetKey; @@ -98,9 +100,6 @@ public void uploadAssetTest() throws Exception { @Test public void hashTest() throws NoSuchAlgorithmException { String assetId = "3-4-22d347d5-4e74-44f7-bf5f-d73838bffd79"; - MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); - messageDigest.update(assetId.getBytes()); - String hash = Base64.encode(messageDigest.digest()); - String encode = UrlEncoded.encodeString(hash); + String key = Helper.key(assetId); } } From 39a2aa966344e498433205d3f26674d5d0e02b95 Mon Sep 17 00:00:00 2001 From: Dejan Date: Mon, 31 Oct 2022 10:30:50 +0100 Subject: [PATCH 32/50] Dont url encode the hash --- .../wire/bots/recording/utils/Collector.java | 1 - .../com/wire/bots/recording/utils/Helper.java | 5 ++--- .../recording/ConversationTemplateTest.java | 9 ++++----- .../assets/Wire 2022-02-27 at 2_15 PM.png | Bin 0 -> 154942 bytes 4 files changed, 6 insertions(+), 9 deletions(-) create mode 100644 src/test/resources/assets/Wire 2022-02-27 at 2_15 PM.png diff --git a/src/main/java/com/wire/bots/recording/utils/Collector.java b/src/main/java/com/wire/bots/recording/utils/Collector.java index 58853d6..9791a35 100644 --- a/src/main/java/com/wire/bots/recording/utils/Collector.java +++ b/src/main/java/com/wire/bots/recording/utils/Collector.java @@ -353,7 +353,6 @@ public void setConversationId(UUID conversationId) { this.conversationId = conversationId; } - public String getConvName() { return convName; } diff --git a/src/main/java/com/wire/bots/recording/utils/Helper.java b/src/main/java/com/wire/bots/recording/utils/Helper.java index 7cbb183..0a33a5b 100644 --- a/src/main/java/com/wire/bots/recording/utils/Helper.java +++ b/src/main/java/com/wire/bots/recording/utils/Helper.java @@ -6,7 +6,6 @@ import org.commonmark.node.Node; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; -import org.eclipse.jetty.util.UrlEncoded; import javax.annotation.Nullable; import java.io.DataOutputStream; @@ -88,9 +87,9 @@ public static Long date(@Nullable String date) throws ParseException { } public static String key(String assetId) throws NoSuchAlgorithmException { - MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + MessageDigest messageDigest = MessageDigest.getInstance("SHA-1"); messageDigest.update(assetId.getBytes()); String encode = Base64.getEncoder().encodeToString(messageDigest.digest()); - return UrlEncoded.encodeString(encode); + return encode.replace("/[^a-zA-Z0-9-_]/g", ""); } } diff --git a/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java b/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java index 3762717..0aa99cc 100644 --- a/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java +++ b/src/test/java/com/wire/bots/recording/ConversationTemplateTest.java @@ -43,10 +43,9 @@ private static EditedTextMessage edit(UUID userId, String text, String time) { return ret; } - private static ImageMessage img(UUID userId, String time, String key, String mimeType) { - ImageMessage ret = new ImageMessage(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), "", userId, time); - ret.setAssetKey(key); - ret.setMimeType(mimeType); + private static RemoteMessage img(UUID userId, String time, String key, String mimeType) { + RemoteMessage ret = new RemoteMessage(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID(), "", userId, time, + key, "", null,null); return ret; } @@ -117,7 +116,7 @@ public void templateTest() throws Exception { collector.addSystem("**Dejo** deleted something", friday2, "conversation.otr-message-add.delete-text", UUID.randomUUID()); collector.add(txt(lipis, saturday, "8")); collector.add(quote(dejan, "This was a quote", saturday, seven.getMessageId())); - //collector.add(img(lipis, saturday, "ognjiste2", "image/png")); + collector.add(img(lipis, saturday, "Wire 2022-02-27 at 2_15 PM", "image/png")); //collector.add(img(lipis, saturday, "small", "image/png")); collector.add(txt(dejan, saturday, "9")); collector.add(txt(dejan, saturday, "10")); diff --git a/src/test/resources/assets/Wire 2022-02-27 at 2_15 PM.png b/src/test/resources/assets/Wire 2022-02-27 at 2_15 PM.png new file mode 100644 index 0000000000000000000000000000000000000000..0bf9dbeea91184f8cacc6cad023d036cb0cfba61 GIT binary patch literal 154942 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�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 z%0D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 z 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
YuA>T+#U?gWA;--OlDEB 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 KEOMFu-vj_cLs2CF literal 0 HcmV?d00001 From 5e7648249e4a9f28bde73bb89069b7dc69dddbfd Mon Sep 17 00:00:00 2001 From: Dejan Date: Mon, 31 Oct 2022 13:20:12 +0100 Subject: [PATCH 33/50] Added salt --- recording.yaml | 1 + .../java/com/wire/bots/recording/EventProcessor.java | 8 ++++---- .../java/com/wire/bots/recording/MessageHandler.java | 9 ++++++--- src/main/java/com/wire/bots/recording/model/Config.java | 2 ++ src/main/java/com/wire/bots/recording/utils/Cache.java | 7 +++---- src/main/java/com/wire/bots/recording/utils/Helper.java | 8 +++++--- src/test/java/com/wire/bots/recording/AssetTests.java | 2 +- 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/recording.yaml b/recording.yaml index 27c0c92..018c567 100644 --- a/recording.yaml +++ b/recording.yaml @@ -19,6 +19,7 @@ swagger: token: ${SERVICE_TOKEN:-} apiHost: ${WIRE_API_HOST:-https://prod-nginz-https.wire.com} url: ${PUBLIC_URL:-https://recording.services.wire.com} +salt: ${SALT:-abc} database: driverClass: org.postgresql.Driver diff --git a/src/main/java/com/wire/bots/recording/EventProcessor.java b/src/main/java/com/wire/bots/recording/EventProcessor.java index e08eb9b..6cb2c1b 100644 --- a/src/main/java/com/wire/bots/recording/EventProcessor.java +++ b/src/main/java/com/wire/bots/recording/EventProcessor.java @@ -29,12 +29,12 @@ void clearCache(UUID userId) { File saveHtml(WireClient client, List events, String filename, boolean withPreviews) throws IOException { Collector collector = new Collector(new Cache(client)); for (Event event : events) { - add(client, collector, event, withPreviews); + add(collector, event, withPreviews); } return collector.executeFile(filename); } - private void add(WireClient client, Collector collector, Event event, boolean withPreviews) { + private void add(Collector collector, Event event, boolean withPreviews) { try { switch (event.type) { case "conversation.create": { @@ -42,7 +42,7 @@ private void add(WireClient client, Collector collector, Event event, boolean wi collector.setConvName(msg.conversation.name); collector.setConversationId(msg.convId); - String text = formatConversation(msg, collector.getCache(), client); + String text = formatConversation(msg, collector.getCache()); collector.addSystem(text, msg.time, event.type, msg.id); } break; @@ -149,7 +149,7 @@ class Asset { OriginMessage preview; } - private String formatConversation(SystemMessage msg, Cache cache, WireClient client) { + private String formatConversation(SystemMessage msg, Cache cache) { StringBuilder sb = new StringBuilder(); User user = cache.getUser(msg.from); sb.append(String.format("**%s** started recording in **%s** with: \n", diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index b1c075d..d6f3f79 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -4,6 +4,7 @@ import com.waz.model.Messages; import com.wire.bots.recording.DAO.ChannelsDAO; import com.wire.bots.recording.DAO.EventsDAO; +import com.wire.bots.recording.model.Config; import com.wire.bots.recording.model.Event; import com.wire.bots.recording.model.Log; import com.wire.bots.recording.utils.Helper; @@ -50,11 +51,13 @@ public class MessageHandler extends MessageHandlerBase { private final EventsDAO eventsDAO; private final EventProcessor eventProcessor = new EventProcessor(); + private final Config config; MessageHandler(EventsDAO eventsDAO, ChannelsDAO channelsDAO, StorageFactory storageFactory) { this.eventsDAO = eventsDAO; this.channelsDAO = channelsDAO; this.storageFactory = storageFactory; + config = Service.instance.getConfig(); } void warmup(ClientRepo repo) { @@ -387,8 +390,8 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, } case "/public": { channelsDAO.insert(convId, botId); - String key = Helper.key(convId.toString()); - String text = String.format("%s/channel/%s.html", Service.instance.getConfig().url, key); + String key = Helper.key(convId.toString(), config.salt); + String text = String.format("%s/channel/%s.html", config.url, key); client.send(new MessageText(text), userId); return true; } @@ -405,7 +408,7 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, } private String getConversationPath(UUID convId) throws NoSuchAlgorithmException { - String key = Helper.key(convId.toString()); + String key = Helper.key(convId.toString(), config.salt); return String.format("html/%s.html", key); } diff --git a/src/main/java/com/wire/bots/recording/model/Config.java b/src/main/java/com/wire/bots/recording/model/Config.java index 7becf9d..3b5840c 100644 --- a/src/main/java/com/wire/bots/recording/model/Config.java +++ b/src/main/java/com/wire/bots/recording/model/Config.java @@ -27,4 +27,6 @@ public class Config extends Configuration { @NotNull public String url; + @NotNull + public String salt; } diff --git a/src/main/java/com/wire/bots/recording/utils/Cache.java b/src/main/java/com/wire/bots/recording/utils/Cache.java index 9340eb3..72d613c 100644 --- a/src/main/java/com/wire/bots/recording/utils/Cache.java +++ b/src/main/java/com/wire/bots/recording/utils/Cache.java @@ -1,15 +1,13 @@ package com.wire.bots.recording.utils; +import com.wire.bots.recording.Service; import com.wire.xenon.WireClient; import com.wire.xenon.backend.models.User; import com.wire.xenon.exceptions.HttpException; import com.wire.xenon.models.RemoteMessage; -import org.eclipse.jetty.util.UrlEncoded; import java.io.File; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Base64; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -27,7 +25,8 @@ public static void clear(UUID userId) { } File getAssetFile(RemoteMessage message) throws NoSuchAlgorithmException { - String key = Helper.key(message.getAssetId()); + String salt = Service.instance.getConfig().salt; + String key = Helper.key(message.getAssetId(), salt); return assetsMap.computeIfAbsent(key, k -> { try { byte[] image = client.downloadAsset(message.getAssetId(), diff --git a/src/main/java/com/wire/bots/recording/utils/Helper.java b/src/main/java/com/wire/bots/recording/utils/Helper.java index 0a33a5b..4786d6a 100644 --- a/src/main/java/com/wire/bots/recording/utils/Helper.java +++ b/src/main/java/com/wire/bots/recording/utils/Helper.java @@ -86,10 +86,12 @@ public static Long date(@Nullable String date) throws ParseException { return ret.getTime(); } - public static String key(String assetId) throws NoSuchAlgorithmException { + public static String key(String assetId, String salt) throws NoSuchAlgorithmException { MessageDigest messageDigest = MessageDigest.getInstance("SHA-1"); - messageDigest.update(assetId.getBytes()); + String value = salt + assetId + salt; + messageDigest.update(value.getBytes()); String encode = Base64.getEncoder().encodeToString(messageDigest.digest()); - return encode.replace("/[^a-zA-Z0-9-_]/g", ""); + String replace = encode.replaceAll("[^a-zA-Z0-9]?", ""); + return replace; } } diff --git a/src/test/java/com/wire/bots/recording/AssetTests.java b/src/test/java/com/wire/bots/recording/AssetTests.java index 249bc6d..6eb4995 100644 --- a/src/test/java/com/wire/bots/recording/AssetTests.java +++ b/src/test/java/com/wire/bots/recording/AssetTests.java @@ -100,6 +100,6 @@ public void uploadAssetTest() throws Exception { @Test public void hashTest() throws NoSuchAlgorithmException { String assetId = "3-4-22d347d5-4e74-44f7-bf5f-d73838bffd79"; - String key = Helper.key(assetId); + String key = Helper.key(assetId, "abc"); } } From e598a2b305b7a6ce75fe0f4c07b78a21c32dfe9e Mon Sep 17 00:00:00 2001 From: Dejan Date: Mon, 31 Oct 2022 15:21:03 +0100 Subject: [PATCH 34/50] Store all assets for the conv in a separate dire --- .../com/wire/bots/recording/utils/Cache.java | 7 ++++--- .../com/wire/bots/recording/utils/Helper.java | 16 ++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/utils/Cache.java b/src/main/java/com/wire/bots/recording/utils/Cache.java index 72d613c..4993365 100644 --- a/src/main/java/com/wire/bots/recording/utils/Cache.java +++ b/src/main/java/com/wire/bots/recording/utils/Cache.java @@ -26,14 +26,15 @@ public static void clear(UUID userId) { File getAssetFile(RemoteMessage message) throws NoSuchAlgorithmException { String salt = Service.instance.getConfig().salt; - String key = Helper.key(message.getAssetId(), salt); - return assetsMap.computeIfAbsent(key, k -> { + String assetKey = Helper.key(message.getAssetId(), salt); + return assetsMap.computeIfAbsent(assetKey, k -> { try { + String convKey = Helper.key(message.getConversationId().toString(), salt); byte[] image = client.downloadAsset(message.getAssetId(), message.getAssetToken(), message.getSha256(), message.getOtrKey()); - return Helper.saveAsset(image, key); + return Helper.saveAsset(convKey, image, assetKey); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/src/main/java/com/wire/bots/recording/utils/Helper.java b/src/main/java/com/wire/bots/recording/utils/Helper.java index 4786d6a..0f90141 100644 --- a/src/main/java/com/wire/bots/recording/utils/Helper.java +++ b/src/main/java/com/wire/bots/recording/utils/Helper.java @@ -16,10 +16,7 @@ import java.security.NoSuchAlgorithmException; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.Base64; -import java.util.Collections; -import java.util.Date; -import java.util.List; +import java.util.*; public class Helper { private static final List extensions = Collections.singletonList(AutolinkExtension.create()); @@ -37,8 +34,8 @@ static File saveProfileAsset(byte[] image, String key) throws Exception { return save(image, file); } - static File saveAsset(byte[] image, String key) throws Exception { - File file = assetFile(key, "image/jpeg"); + static File saveAsset(String convKey, byte[] image, String assetKey) throws Exception { + File file = assetFile(convKey, assetKey, "image/jpeg"); return save(image, file); } @@ -50,11 +47,14 @@ private static File save(byte[] image, File file) throws IOException { return file; } - static File assetFile(String assetKey, String mimeType) { + static File assetFile(String convKey, String assetKey, String mimeType) { String extension = getExtension(mimeType); if (extension.isEmpty()) extension = "error"; - String filename = String.format("assets/%s.%s", assetKey, extension); + String dirName = String.format("assets/%s", convKey); + File dir = new File(dirName); + dir.mkdir(); + String filename = String.format("%s/%s.%s", dirName, assetKey, extension); return new File(filename); } From 5a07114f590feda8b2ce486a0d9ad0b21b4d339f Mon Sep 17 00:00:00 2001 From: Dejan Date: Mon, 31 Oct 2022 15:35:46 +0100 Subject: [PATCH 35/50] File path in Collector --- src/main/java/com/wire/bots/recording/utils/Collector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/wire/bots/recording/utils/Collector.java b/src/main/java/com/wire/bots/recording/utils/Collector.java index 9791a35..ff3733b 100644 --- a/src/main/java/com/wire/bots/recording/utils/Collector.java +++ b/src/main/java/com/wire/bots/recording/utils/Collector.java @@ -108,7 +108,7 @@ public void add(RemoteMessage event) throws Exception { message.timeStamp = event.getTime(); File file = cache.getAssetFile(event); - message.image = getFilename(file); + message.image = "/" + file.getPath(); //getFilename(file); Sender sender = sender(event.getUserId()); sender.add(message); From d078bcfcd08286ad919928460f8a02db6ea2c916 Mon Sep 17 00:00:00 2001 From: Dejan Date: Mon, 31 Oct 2022 15:59:31 +0100 Subject: [PATCH 36/50] Delete asset files upon /private --- .../wire/bots/recording/MessageHandler.java | 30 +++++++++++++++++-- .../wire/bots/recording/utils/Collector.java | 9 ++---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index d6f3f79..9182067 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -397,9 +397,17 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, } case "/private": { channelsDAO.delete(convId); + + // Delete downloaded assets + File assetDir = getAssetDir(convId); + deleteDir(assetDir); + + // Delete the html file String filename = getConversationPath(convId); - boolean delete = new File(filename).delete(); - String txt = String.format("%s deleted: %s", filename, delete); + File htmlFile = new File(filename); + boolean delete = htmlFile.delete(); + + String txt = String.format("%s deleted: %s", htmlFile.getPath(), delete); client.send(new MessageText(txt), userId); return true; } @@ -407,6 +415,24 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, return false; } + private void deleteDir(File assetDir) { + File[] files = assetDir.listFiles(); + if (files == null) + return; + + for (File f : files) { + if (f.isFile()) { + boolean delete = f.delete(); + Logger.info("Deleted file: %s %s", f.getAbsolutePath(), delete); + } + } + } + + private File getAssetDir(UUID convId) throws NoSuchAlgorithmException { + String key = Helper.key(convId.toString(), config.salt); + return new File(String.format("assets/%s", key)); + } + private String getConversationPath(UUID convId) throws NoSuchAlgorithmException { String key = Helper.key(convId.toString(), config.salt); return String.format("html/%s.html", key); diff --git a/src/main/java/com/wire/bots/recording/utils/Collector.java b/src/main/java/com/wire/bots/recording/utils/Collector.java index ff3733b..cd4a1c7 100644 --- a/src/main/java/com/wire/bots/recording/utils/Collector.java +++ b/src/main/java/com/wire/bots/recording/utils/Collector.java @@ -123,7 +123,7 @@ public void add(RemoteMessage event, VideoPreviewMessage preview) throws Excepti File file = cache.getAssetFile(event); message.video = new Video(); - message.video.url = getFilename(file); + message.video.url = "/" + file.getPath(); //getFilename(file); message.video.width = preview.getWidth(); message.video.height = preview.getHeight(); message.video.mimeType = preview.getMimeType(); @@ -140,7 +140,7 @@ public Sender add(RemoteMessage event, FilePreviewMessage preview) throws Except message.timeStamp = event.getTime(); File file = cache.getAssetFile(event); - String assetFilename = getFilename(file); + String assetFilename = "/" + file.getPath(); //getFilename(file); message.attachment = new Attachment(); message.attachment.name = String.format("%s (%s)", preview.getName(), event.getAssetId()); @@ -301,11 +301,6 @@ private Sender append(Sender sender, Message message, String dateTime) throws Pa return days.getLast().senders.getLast(); } - private String getFilename(File file) { - - return String.format("/%s/%s", "assets", file.getName()); - } - @Nullable private String getAvatar(UUID userId) { User user = cache.getUser(userId); From 0cba6d3b965f67157d61cc0f5b65c25f96e58830 Mon Sep 17 00:00:00 2001 From: Dejan Date: Tue, 1 Nov 2022 11:56:11 +0100 Subject: [PATCH 37/50] ImageBundle media type --- recording.yaml | 2 +- src/main/java/com/wire/bots/recording/Service.java | 2 +- src/main/java/com/wire/bots/recording/utils/ImagesBundle.java | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/recording.yaml b/recording.yaml index 018c567..0f5a92b 100644 --- a/recording.yaml +++ b/recording.yaml @@ -8,7 +8,7 @@ server: logging: level: INFO loggers: - "com.wire.bots.logger": ${LOG_LEVEL:-INFO} + "com.wire.xenon.tools.logger": ${LOG_LEVEL:-INFO} swagger: resourcePackage: com.wire.bots.sdk.server.resources diff --git a/src/main/java/com/wire/bots/recording/Service.java b/src/main/java/com/wire/bots/recording/Service.java index 43bbc52..ab6ef16 100644 --- a/src/main/java/com/wire/bots/recording/Service.java +++ b/src/main/java/com/wire/bots/recording/Service.java @@ -49,7 +49,7 @@ public void initialize(Bootstrap bootstrap) { bootstrap.addBundle(new AssetsBundle("/scripts", "/scripts", "index.htm", "scripts")); bootstrap.addBundle(new ImagesBundle(workingDir + "/avatars", "/avatars", "avatars")); bootstrap.addBundle(new ImagesBundle(workingDir + "/html", "/channel", "channels")); - bootstrap.addBundle(new ImagesBundle(workingDir + "/assets", "/assets", "assets")); + bootstrap.addBundle(new ImagesBundle(workingDir + "/assets", "/assets", "assets", "application/octet-stream")); Application application = bootstrap.getApplication(); instance = (Service) application; diff --git a/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java b/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java index 373e515..c0012e9 100644 --- a/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java +++ b/src/main/java/com/wire/bots/recording/utils/ImagesBundle.java @@ -16,6 +16,10 @@ import java.nio.charset.StandardCharsets; public class ImagesBundle extends AssetsBundle { + public ImagesBundle(String resourcePath, String uriPath, String name, String mediaType) { + super(resourcePath, uriPath, "index.htm", name, mediaType); + } + public ImagesBundle(String resourcePath, String uriPath, String name) { super(resourcePath, uriPath, "index.htm", name); } From d5e0ea323aa7e7e91bffca4ec25c2b1b166aa727 Mon Sep 17 00:00:00 2001 From: Dejan Date: Tue, 1 Nov 2022 13:54:18 +0100 Subject: [PATCH 38/50] imply meme type from the asset content --- src/main/java/com/wire/bots/recording/utils/Cache.java | 8 ++++++-- src/main/java/com/wire/bots/recording/utils/Helper.java | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/utils/Cache.java b/src/main/java/com/wire/bots/recording/utils/Cache.java index 4993365..4d9b9b0 100644 --- a/src/main/java/com/wire/bots/recording/utils/Cache.java +++ b/src/main/java/com/wire/bots/recording/utils/Cache.java @@ -5,6 +5,7 @@ import com.wire.xenon.backend.models.User; import com.wire.xenon.exceptions.HttpException; import com.wire.xenon.models.RemoteMessage; +import com.wire.xenon.tools.Util; import java.io.File; import java.security.NoSuchAlgorithmException; @@ -30,11 +31,14 @@ File getAssetFile(RemoteMessage message) throws NoSuchAlgorithmException { return assetsMap.computeIfAbsent(assetKey, k -> { try { String convKey = Helper.key(message.getConversationId().toString(), salt); - byte[] image = client.downloadAsset(message.getAssetId(), + byte[] asset = client.downloadAsset(message.getAssetId(), message.getAssetToken(), message.getSha256(), message.getOtrKey()); - return Helper.saveAsset(convKey, image, assetKey); + String mimeType = Util.extractMimeType(asset); + if (mimeType == null) + mimeType = "image/jpeg"; + return Helper.saveAsset(convKey, asset, assetKey, mimeType); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/src/main/java/com/wire/bots/recording/utils/Helper.java b/src/main/java/com/wire/bots/recording/utils/Helper.java index 0f90141..e3a8d55 100644 --- a/src/main/java/com/wire/bots/recording/utils/Helper.java +++ b/src/main/java/com/wire/bots/recording/utils/Helper.java @@ -34,8 +34,8 @@ static File saveProfileAsset(byte[] image, String key) throws Exception { return save(image, file); } - static File saveAsset(String convKey, byte[] image, String assetKey) throws Exception { - File file = assetFile(convKey, assetKey, "image/jpeg"); + static File saveAsset(String convKey, byte[] image, String assetKey, String mimeType) throws Exception { + File file = assetFile(convKey, assetKey, mimeType); return save(image, file); } From a8e3bc1198dd1e99e46676466ef6a7a39636c82f Mon Sep 17 00:00:00 2001 From: Dejan Date: Tue, 1 Nov 2022 14:21:48 +0100 Subject: [PATCH 39/50] FileAttachments --- .../com/wire/bots/recording/utils/Cache.java | 2 -- .../wire/bots/recording/utils/Collector.java | 19 ++++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/utils/Cache.java b/src/main/java/com/wire/bots/recording/utils/Cache.java index 4d9b9b0..b755b4a 100644 --- a/src/main/java/com/wire/bots/recording/utils/Cache.java +++ b/src/main/java/com/wire/bots/recording/utils/Cache.java @@ -36,8 +36,6 @@ File getAssetFile(RemoteMessage message) throws NoSuchAlgorithmException { message.getSha256(), message.getOtrKey()); String mimeType = Util.extractMimeType(asset); - if (mimeType == null) - mimeType = "image/jpeg"; return Helper.saveAsset(convKey, asset, assetKey, mimeType); } catch (Exception e) { throw new RuntimeException(e); diff --git a/src/main/java/com/wire/bots/recording/utils/Collector.java b/src/main/java/com/wire/bots/recording/utils/Collector.java index cd4a1c7..23338ea 100644 --- a/src/main/java/com/wire/bots/recording/utils/Collector.java +++ b/src/main/java/com/wire/bots/recording/utils/Collector.java @@ -108,7 +108,16 @@ public void add(RemoteMessage event) throws Exception { message.timeStamp = event.getTime(); File file = cache.getAssetFile(event); - message.image = "/" + file.getPath(); //getFilename(file); + + if (file.getName().endsWith(".xyz")) { + // attachment + message.attachment = new Attachment(); + message.attachment.name = String.format("%s (%s)", file.getName(), event.getAssetId()); + message.attachment.url = "file:///" + file.getPath(); + } else { + // image + message.image = "/" + file.getPath(); + } Sender sender = sender(event.getUserId()); sender.add(message); @@ -123,7 +132,7 @@ public void add(RemoteMessage event, VideoPreviewMessage preview) throws Excepti File file = cache.getAssetFile(event); message.video = new Video(); - message.video.url = "/" + file.getPath(); //getFilename(file); + message.video.url = "/" + file.getPath(); message.video.width = preview.getWidth(); message.video.height = preview.getHeight(); message.video.mimeType = preview.getMimeType(); @@ -134,16 +143,16 @@ public void add(RemoteMessage event, VideoPreviewMessage preview) throws Excepti append(sender, message, event.getTime()); } - public Sender add(RemoteMessage event, FilePreviewMessage preview) throws Exception { + public Sender add(RemoteMessage event, String name) throws Exception { Message message = new Message(); message.id = event.getMessageId(); message.timeStamp = event.getTime(); File file = cache.getAssetFile(event); - String assetFilename = "/" + file.getPath(); //getFilename(file); + String assetFilename = "/" + file.getPath(); message.attachment = new Attachment(); - message.attachment.name = String.format("%s (%s)", preview.getName(), event.getAssetId()); + message.attachment.name = String.format("%s (%s)", name, event.getAssetId()); message.attachment.url = "file://" + assetFilename; Sender sender = sender(event.getUserId()); From 76b745ad7d2a8585e30ed69c9a4a388baf633277 Mon Sep 17 00:00:00 2001 From: Dejan Date: Tue, 1 Nov 2022 15:15:48 +0100 Subject: [PATCH 40/50] FileAttachments --- src/main/java/com/wire/bots/recording/utils/Collector.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/utils/Collector.java b/src/main/java/com/wire/bots/recording/utils/Collector.java index 23338ea..7801ad0 100644 --- a/src/main/java/com/wire/bots/recording/utils/Collector.java +++ b/src/main/java/com/wire/bots/recording/utils/Collector.java @@ -112,8 +112,8 @@ public void add(RemoteMessage event) throws Exception { if (file.getName().endsWith(".xyz")) { // attachment message.attachment = new Attachment(); - message.attachment.name = String.format("%s (%s)", file.getName(), event.getAssetId()); - message.attachment.url = "file:///" + file.getPath(); + message.attachment.name = String.format("%s", file.getName()); + message.attachment.url = "/" + file.getPath(); } else { // image message.image = "/" + file.getPath(); From 981c429fbccd8dcd9947e37958067d7ebc68d338 Mon Sep 17 00:00:00 2001 From: Dejan Date: Tue, 1 Nov 2022 15:34:10 +0100 Subject: [PATCH 41/50] Disabled markdowns --- src/main/java/com/wire/bots/recording/utils/Collector.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/utils/Collector.java b/src/main/java/com/wire/bots/recording/utils/Collector.java index 7801ad0..f62d72f 100644 --- a/src/main/java/com/wire/bots/recording/utils/Collector.java +++ b/src/main/java/com/wire/bots/recording/utils/Collector.java @@ -220,7 +220,7 @@ public void addSystem(String text, String dateTime, String type, UUID msgId) thr Message message = new Message(); message.id = msgId; - message.text = Helper.markdown2Html(text); + message.text = text; //Helper.markdown2Html(text); message.timeStamp = dateTime; Sender sender = system(type); @@ -231,7 +231,7 @@ public void addSystem(String text, String dateTime, String type, UUID msgId) thr private String getText(TextMessage event) { String text = event.getText(); - return Helper.markdown2Html(text); + return text;// Helper.markdown2Html(text); } @Nullable From 7eb76818f518c4d6ed5575a68d9d75d2e52683d6 Mon Sep 17 00:00:00 2001 From: Dejan Date: Tue, 1 Nov 2022 15:52:22 +0100 Subject: [PATCH 42/50] more logging --- src/main/java/com/wire/bots/recording/MessageHandler.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index 9182067..bfbe100 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -384,6 +384,11 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, fileAsset.setAssetKey(assetKey.id); fileAsset.setAssetToken(assetKey.token); + Logger.info("Asset: key: %s, token: %s, msg: %s", + fileAsset.getAssetKey(), + fileAsset.getAssetToken(), + fileAsset.getMessageId()); + // Post Asset client.send(fileAsset, userId); return true; From b049301e0016aba7f15007a3db332cb4560321c5 Mon Sep 17 00:00:00 2001 From: Dejan Date: Tue, 1 Nov 2022 17:43:55 +0100 Subject: [PATCH 43/50] Lithium 3.4.5 --- pom.xml | 2 +- src/main/java/com/wire/bots/recording/MessageHandler.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 86a0e6b..ef1d8a5 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ UTF-8 2.1.4 - 3.4.4 + 3.4.5 1.0.10 0.16.0 0.17.0 diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index bfbe100..eb374c4 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -383,11 +383,12 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, AssetKey assetKey = client.uploadAsset(fileAsset); fileAsset.setAssetKey(assetKey.id); fileAsset.setAssetToken(assetKey.token); + fileAsset.setDomain(assetKey.domain); - Logger.info("Asset: key: %s, token: %s, msg: %s", + Logger.info("Asset: key: %s, token: %s, domain: %s", fileAsset.getAssetKey(), fileAsset.getAssetToken(), - fileAsset.getMessageId()); + fileAsset.getDomain()); // Post Asset client.send(fileAsset, userId); From 0710ee9522ba5978b6b4e6fcf2a46ccca4bc4161 Mon Sep 17 00:00:00 2001 From: Dejan Date: Tue, 1 Nov 2022 18:13:47 +0100 Subject: [PATCH 44/50] BaseUrl for PDF --- src/main/java/com/wire/bots/recording/MessageHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index eb374c4..9492f32 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -370,7 +370,8 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, String convName = client.getConversation().name; String pdfFilename = String.format("html/%s.pdf", URLEncoder.encode(convName, StandardCharsets.UTF_8)); - File pdfFile = PdfGenerator.save(pdfFilename, html, "file:/opt"); + String baseUrl = "file:/opt/recording"; + File pdfFile = PdfGenerator.save(pdfFilename, html, baseUrl); // Post the Preview UUID messageId = UUID.randomUUID(); From 5b5beca280a2b97df489857f44c3afcb72dcc4f0 Mon Sep 17 00:00:00 2001 From: Dejan Date: Wed, 2 Nov 2022 10:50:46 +0100 Subject: [PATCH 45/50] NPE in /pdf --- src/main/java/com/wire/bots/recording/MessageHandler.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/wire/bots/recording/MessageHandler.java b/src/main/java/com/wire/bots/recording/MessageHandler.java index 9492f32..9dbc0da 100644 --- a/src/main/java/com/wire/bots/recording/MessageHandler.java +++ b/src/main/java/com/wire/bots/recording/MessageHandler.java @@ -369,6 +369,10 @@ private boolean command(WireClient client, UUID userId, UUID botId, UUID convId, String html = Util.readFile(file); String convName = client.getConversation().name; + if(convName == null){ + convName = "Recording"; + } + String pdfFilename = String.format("html/%s.pdf", URLEncoder.encode(convName, StandardCharsets.UTF_8)); String baseUrl = "file:/opt/recording"; File pdfFile = PdfGenerator.save(pdfFilename, html, baseUrl); From c828fc50f1a60f4709fdfb0896d4a4f1d903d0c1 Mon Sep 17 00:00:00 2001 From: Dejan Date: Wed, 2 Nov 2022 11:13:44 +0100 Subject: [PATCH 46/50] Enabled markdown --- src/main/java/com/wire/bots/recording/utils/Collector.java | 4 ++-- src/main/java/com/wire/bots/recording/utils/Helper.java | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/wire/bots/recording/utils/Collector.java b/src/main/java/com/wire/bots/recording/utils/Collector.java index f62d72f..7801ad0 100644 --- a/src/main/java/com/wire/bots/recording/utils/Collector.java +++ b/src/main/java/com/wire/bots/recording/utils/Collector.java @@ -220,7 +220,7 @@ public void addSystem(String text, String dateTime, String type, UUID msgId) thr Message message = new Message(); message.id = msgId; - message.text = text; //Helper.markdown2Html(text); + message.text = Helper.markdown2Html(text); message.timeStamp = dateTime; Sender sender = system(type); @@ -231,7 +231,7 @@ public void addSystem(String text, String dateTime, String type, UUID msgId) thr private String getText(TextMessage event) { String text = event.getText(); - return text;// Helper.markdown2Html(text); + return Helper.markdown2Html(text); } @Nullable diff --git a/src/main/java/com/wire/bots/recording/utils/Helper.java b/src/main/java/com/wire/bots/recording/utils/Helper.java index e3a8d55..68a9edc 100644 --- a/src/main/java/com/wire/bots/recording/utils/Helper.java +++ b/src/main/java/com/wire/bots/recording/utils/Helper.java @@ -91,7 +91,6 @@ public static String key(String assetId, String salt) throws NoSuchAlgorithmExce String value = salt + assetId + salt; messageDigest.update(value.getBytes()); String encode = Base64.getEncoder().encodeToString(messageDigest.digest()); - String replace = encode.replaceAll("[^a-zA-Z0-9]?", ""); - return replace; + return encode.replaceAll("[^a-zA-Z0-9]?", ""); } } From 67006bc03f5ea4d6b0a9e52e2784086f20d41583 Mon Sep 17 00:00:00 2001 From: Dejan Date: Wed, 2 Nov 2022 11:17:45 +0100 Subject: [PATCH 47/50] template --- src/main/resources/templates/conversation.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/resources/templates/conversation.html b/src/main/resources/templates/conversation.html index 1c870a0..c84a647 100644 --- a/src/main/resources/templates/conversation.html +++ b/src/main/resources/templates/conversation.html @@ -189,12 +189,12 @@

{{ title }}

{{#messages}} -
+ {{#quotedMessage}}
{{ name }} {{{ text }}} - Original message from {{ date }} + Original message from {{ date }}
{{/quotedMessage}} {{{ text }}} @@ -202,7 +202,7 @@

{{ title }}

{{/image}} {{#attachment}} - {{ name }} + {{ name }} {{/attachment}} {{#link}}
@@ -210,7 +210,7 @@

{{ title }}

{{ title }}

- {{ url }} + {{ url }}
{{/link}} {{#likes}} From 540c06ca5162351877a2b10cb69c3626ae58aa69 Mon Sep 17 00:00:00 2001 From: Dejan Date: Wed, 9 Nov 2022 15:28:12 +0100 Subject: [PATCH 48/50] sanitizeUrls(true) --- src/main/java/com/wire/bots/recording/utils/Helper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/wire/bots/recording/utils/Helper.java b/src/main/java/com/wire/bots/recording/utils/Helper.java index 68a9edc..f5819b4 100644 --- a/src/main/java/com/wire/bots/recording/utils/Helper.java +++ b/src/main/java/com/wire/bots/recording/utils/Helper.java @@ -76,6 +76,7 @@ static String markdown2Html(@Nullable String text) { .builder() .escapeHtml(true) .extensions(extensions) + .sanitizeUrls(true) .build() .render(document); } From b5acac094881e77e19876f145cdacfa31b701559 Mon Sep 17 00:00:00 2001 From: Dejan Date: Thu, 10 Nov 2022 17:39:39 +0100 Subject: [PATCH 49/50] Display attachment name --- src/main/java/com/wire/bots/recording/EventProcessor.java | 2 +- src/main/java/com/wire/bots/recording/utils/Collector.java | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/wire/bots/recording/EventProcessor.java b/src/main/java/com/wire/bots/recording/EventProcessor.java index 6cb2c1b..de0c7d7 100644 --- a/src/main/java/com/wire/bots/recording/EventProcessor.java +++ b/src/main/java/com/wire/bots/recording/EventProcessor.java @@ -70,7 +70,7 @@ private void add(Collector collector, Event event, boolean withPreviews) { break; case "conversation.otr-message-add.file-preview": { FilePreviewMessage message = mapper.readValue(event.payload, FilePreviewMessage.class); - //collector.add(message); + collector.add(message); } break; case "conversation.otr-message-add.image-preview": { diff --git a/src/main/java/com/wire/bots/recording/utils/Collector.java b/src/main/java/com/wire/bots/recording/utils/Collector.java index 7801ad0..f3e078b 100644 --- a/src/main/java/com/wire/bots/recording/utils/Collector.java +++ b/src/main/java/com/wire/bots/recording/utils/Collector.java @@ -125,6 +125,10 @@ public void add(RemoteMessage event) throws Exception { append(sender, message, event.getTime()); } + public void add(FilePreviewMessage message) throws ParseException { + addSystem(message.getName(), message.getTime(), "file-preview", message.getMessageId()); + } + public void add(RemoteMessage event, VideoPreviewMessage preview) throws Exception { Message message = new Message(); message.id = event.getMessageId(); From 449af2cab100a10d863fd307789c99e6e415aa99 Mon Sep 17 00:00:00 2001 From: Dejan Date: Fri, 11 Nov 2022 10:51:22 +0100 Subject: [PATCH 50/50] Pom.xml --- pom.xml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index c1b0c9f..83a95c8 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ UTF-8 2.1.4 - 3.4.5 + 3.4.6 1.0.10 0.16.0 0.17.0 @@ -76,12 +76,11 @@ com.atlassian.commonmark commonmark-ext-autolink ${commonmark.version} - 0.12.1 junit junit - 4.13.1 + 4.13.2 test @@ -104,12 +103,6 @@ zip4j ${zip4j.version} - - junit - junit - 4.13.2 - test -