From 0e0dc5c1483e6737e7d9c851620f46fdd40db774 Mon Sep 17 00:00:00 2001 From: Christian 'ketura' McCarty Date: Sat, 30 Nov 2024 18:36:42 -0600 Subject: [PATCH] Extracting markdown parsing out of the chat request handler and putting it in its own class so it can be used by other processes --- .../async/handler/ChatRequestHandler.java | 112 ++---------------- .../gempukku/lotro/builder/DaoBuilder.java | 4 +- .../gempukku/lotro/builder/ServerBuilder.java | 4 + .../com/gempukku/lotro/chat/ChatServer.java | 12 +- .../gempukku/lotro/chat/MarkdownParser.java | 106 +++++++++++++++++ 5 files changed, 127 insertions(+), 111 deletions(-) create mode 100644 gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/chat/MarkdownParser.java diff --git a/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/ChatRequestHandler.java b/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/ChatRequestHandler.java index d42e3de77..7baffb262 100644 --- a/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/ChatRequestHandler.java +++ b/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/ChatRequestHandler.java @@ -4,10 +4,7 @@ import com.gempukku.lotro.PrivateInformationException; import com.gempukku.lotro.SubscriptionExpiredException; import com.gempukku.lotro.async.HttpProcessingException; import com.gempukku.lotro.async.ResponseWriter; -import com.gempukku.lotro.chat.ChatCommandErrorException; -import com.gempukku.lotro.chat.ChatMessage; -import com.gempukku.lotro.chat.ChatRoomMediator; -import com.gempukku.lotro.chat.ChatServer; +import com.gempukku.lotro.chat.*; import com.gempukku.lotro.game.ChatCommunicationChannel; import com.gempukku.lotro.game.Player; import com.gempukku.polling.LongPollingResource; @@ -17,18 +14,7 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger;import org.commonmark.Extension; -import org.commonmark.ext.autolink.AutolinkExtension; -import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; -import org.commonmark.node.Image; -import org.commonmark.node.Link; -import org.commonmark.node.Node; -import org.commonmark.node.Text; -import org.commonmark.parser.Parser; -import org.commonmark.renderer.NodeRenderer; -import org.commonmark.renderer.html.HtmlNodeRendererContext; -import org.commonmark.renderer.html.HtmlRenderer; -import org.commonmark.renderer.html.HtmlWriter; +import org.apache.logging.log4j.Logger; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -38,32 +24,19 @@ import java.lang.reflect.Type; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.*; -import java.util.regex.Pattern; public class ChatRequestHandler extends LotroServerRequestHandler implements UriRequestHandler { private final ChatServer _chatServer; - private final LongPollingSystem longPollingSystem; - private final Parser _markdownParser; - private final HtmlRenderer _markdownRenderer; + private final LongPollingSystem _longPollingSystem; + private final MarkdownParser _markdownParser; private static final Logger _log = LogManager.getLogger(ChatRequestHandler.class); public ChatRequestHandler(Map context, LongPollingSystem longPollingSystem) { super(context); _chatServer = extractObject(context, ChatServer.class); - this.longPollingSystem = longPollingSystem; - - List adminExt = Arrays.asList(StrikethroughExtension.create(), AutolinkExtension.create()); - _markdownParser = Parser.builder() - .extensions(adminExt) - .build(); - _markdownRenderer = HtmlRenderer.builder() - .nodeRendererFactory(htmlContext -> new LinkShredder(htmlContext)) - .extensions(adminExt) - .escapeHtml(true) - .sanitizeUrls(true) - .softbreak("
") - .build(); + _markdownParser = extractObject(context, MarkdownParser.class); + _longPollingSystem = longPollingSystem; } @Override @@ -77,8 +50,6 @@ public class ChatRequestHandler extends LotroServerRequestHandler implements Uri } } - private Pattern QuoteExtender = Pattern.compile("^([ \t]*>[ \t]*.+)(?=\n[ \t]*[^>])", Pattern.MULTILINE); - private void postMessages(HttpRequest request, String room, ResponseWriter responseWriter) throws Exception { HttpPostRequestDecoder postDecoder = new HttpPostRequestDecoder(request); try { @@ -94,29 +65,14 @@ public class ChatRequestHandler extends LotroServerRequestHandler implements Uri try { final boolean admin = resourceOwner.hasType(Player.Type.ADMIN); final boolean leagueAdmin = resourceOwner.hasType(Player.Type.LEAGUE_ADMIN); - if (message != null && message.trim().length() > 0) { - String newMsg; - newMsg = message.trim().replaceAll("\n\n\n+", "\n\n\n"); - newMsg = QuoteExtender.matcher(newMsg).replaceAll("$1\n"); - //Escaping underscores so that URLs with lots of underscores (i.e. wiki links) aren't mangled - // Besides, who uses _this_ instead of *this*? - newMsg = newMsg.replace("_", "\\_"); - - //Need to preserve any commands being made - if(!newMsg.startsWith("/")) { - newMsg = _markdownRenderer.render(_markdownParser.parse(newMsg)); - // Prevent quotes with newlines from displaying side-by-side - newMsg = newMsg.replaceAll("[\n \t]*
", "

"); - //Make all links open in a new tab - newMsg = newMsg.replaceAll("<(a href=\".*?\")>", "<$1 target=\"blank\">"); - } - + if (message != null && !message.trim().isEmpty()) { + String newMsg = _markdownParser.renderMarkdown(message); chatRoom.sendMessage(resourceOwner.getName(), newMsg, admin); responseWriter.writeXmlResponse(null); } else { ChatCommunicationChannel pollableResource = chatRoom.getChatRoomListener(resourceOwner.getName()); ChatUpdateLongPollingResource polledResource = new ChatUpdateLongPollingResource(chatRoom, room, resourceOwner.getName(), admin, responseWriter); - longPollingSystem.processLongPollingResource(polledResource, pollableResource); + _longPollingSystem.processLongPollingResource(polledResource, pollableResource); } } catch (SubscriptionExpiredException exp) { logHttpError(_log, 410, request.uri(), exp); @@ -133,56 +89,6 @@ public class ChatRequestHandler extends LotroServerRequestHandler implements Uri } } - //Processing to implement: - // + quotes restricted to one line - // - triple quote to avoid this?? - // + remove url text processing - // + remove image processing - // - re-enable bare url linking - - private class LinkShredder implements NodeRenderer { - - private final HtmlWriter html; - - LinkShredder(HtmlNodeRendererContext context) { - this.html = context.getWriter(); - } - - @Override - public Set> getNodeTypes() { - // Return the node types we want to use this renderer for. - return new HashSet<>(Arrays.asList( - Link.class, - Image.class - )); - } - - @Override - public void render(Node node) { - if(node instanceof Link link) { - if(link.getTitle() != null) { - html.text(link.getTitle() + ": " + link.getDestination()); - } - else { - if(link.getFirstChild() != null - && link.getFirstChild() instanceof Text text - && !text.getLiteral().equals(link.getDestination())) - { - html.text(text.getLiteral() + ": " + link.getDestination()); - } - else { - html.tag("a", Collections.singletonMap("href", link.getDestination())); - html.text(link.getDestination()); - html.tag("/a"); - } - } - - } - else if(node instanceof Image image){ - html.text(image.getTitle() + ": " + image.getDestination()); - } - } - } private class ChatUpdateLongPollingResource implements LongPollingResource { private final ChatRoomMediator chatRoom; diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/builder/DaoBuilder.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/builder/DaoBuilder.java index a29d38081..53cf8b3b3 100644 --- a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/builder/DaoBuilder.java +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/builder/DaoBuilder.java @@ -1,6 +1,7 @@ package com.gempukku.lotro.builder; import com.gempukku.lotro.cache.CacheManager; +import com.gempukku.lotro.chat.MarkdownParser; import com.gempukku.lotro.collection.CachedCollectionDAO; import com.gempukku.lotro.collection.CachedTransferDAO; import com.gempukku.lotro.collection.CollectionSerializer; @@ -8,7 +9,6 @@ import com.gempukku.lotro.collection.TransferDAO; import com.gempukku.lotro.db.*; import com.gempukku.lotro.game.LotroCardBlueprintLibrary; import com.gempukku.lotro.log.LoggingProxy; -import com.gempukku.lotro.packs.ProductLibrary; import com.gempukku.lotro.tournament.TournamentDAO; import com.gempukku.lotro.tournament.TournamentMatchDAO; import com.gempukku.lotro.tournament.TournamentPlayerDAO; @@ -50,7 +50,7 @@ public class DaoBuilder { objectMap.put(PlayerDAO.class, playerDao); TransferDAO dbTransferDao = LoggingProxy.createLoggingProxy(TransferDAO.class, new DbTransferDAO(dbAccess)); - CachedTransferDAO transferDao = new CachedTransferDAO(dbTransferDao); + CachedTransferDAO transferDao = new CachedTransferDAO(dbTransferDao, extract(objectMap, MarkdownParser.class)); objectMap.put(TransferDAO.class, transferDao); IpBanDAO dbIpBanDao = LoggingProxy.createLoggingProxy(IpBanDAO.class, new DbIpBanDAO(dbAccess)); diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/builder/ServerBuilder.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/builder/ServerBuilder.java index 03c9f2cf5..a700f2e4a 100644 --- a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/builder/ServerBuilder.java +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/builder/ServerBuilder.java @@ -1,6 +1,7 @@ package com.gempukku.lotro.builder; import com.gempukku.lotro.chat.ChatServer; +import com.gempukku.lotro.chat.MarkdownParser; import com.gempukku.lotro.collection.CollectionSerializer; import com.gempukku.lotro.collection.CollectionsManager; import com.gempukku.lotro.collection.TransferDAO; @@ -32,6 +33,9 @@ public class ServerBuilder { CollectionSerializer collectionSerializer = new CollectionSerializer(); objectMap.put(CollectionSerializer.class, collectionSerializer); + + MarkdownParser parser = new MarkdownParser(); + objectMap.put(MarkdownParser.class, parser); } public static void CreateServices(Map objectMap) { diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/chat/ChatServer.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/chat/ChatServer.java index 7a3bc0238..1a82d4894 100644 --- a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/chat/ChatServer.java +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/chat/ChatServer.java @@ -10,18 +10,18 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class ChatServer extends AbstractServer { - private final IgnoreDAO ignoreDAO; - private final PlayerDAO playerDAO; + private final IgnoreDAO _ignoreDAO; + private final PlayerDAO _playerDAO; private final Map _chatRooms = new ConcurrentHashMap<>(); public ChatServer(IgnoreDAO ignoreDAO, PlayerDAO playerDAO) { - this.ignoreDAO = ignoreDAO; - this.playerDAO = playerDAO; + _ignoreDAO = ignoreDAO; + _playerDAO = playerDAO; } public ChatRoomMediator createChatRoom(String name, boolean muteJoinPartMessages, int secondsTimeoutPeriod, boolean allowIncognito, String welcomeMessage) { - ChatRoomMediator chatRoom = new ChatRoomMediator(ignoreDAO, playerDAO, name, muteJoinPartMessages, secondsTimeoutPeriod, allowIncognito, welcomeMessage); + ChatRoomMediator chatRoom = new ChatRoomMediator(_ignoreDAO, _playerDAO, name, muteJoinPartMessages, secondsTimeoutPeriod, allowIncognito, welcomeMessage); try { chatRoom.sendMessage("System", "Welcome to room: " + name, true); } catch (PrivateInformationException exp) { @@ -34,7 +34,7 @@ public class ChatServer extends AbstractServer { } public ChatRoomMediator createPrivateChatRoom(String name, boolean muteJoinPartMessages, Set allowedUsers, int secondsTimeoutPeriod) { - ChatRoomMediator chatRoom = new ChatRoomMediator(ignoreDAO, playerDAO, name, muteJoinPartMessages, secondsTimeoutPeriod, allowedUsers, false); + ChatRoomMediator chatRoom = new ChatRoomMediator(_ignoreDAO, _playerDAO, name, muteJoinPartMessages, secondsTimeoutPeriod, allowedUsers, false); try { chatRoom.sendMessage("System", "Welcome to private room: " + name, true); } catch (PrivateInformationException exp) { diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/chat/MarkdownParser.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/chat/MarkdownParser.java new file mode 100644 index 000000000..80be5c294 --- /dev/null +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/chat/MarkdownParser.java @@ -0,0 +1,106 @@ +package com.gempukku.lotro.chat; + +import org.commonmark.Extension; +import org.commonmark.ext.autolink.AutolinkExtension; +import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; +import org.commonmark.node.Image; +import org.commonmark.node.Link; +import org.commonmark.node.Node; +import org.commonmark.node.Text; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.NodeRenderer; +import org.commonmark.renderer.html.HtmlNodeRendererContext; +import org.commonmark.renderer.html.HtmlRenderer; +import org.commonmark.renderer.html.HtmlWriter; + +import java.util.*; +import java.util.regex.Pattern; + +public class MarkdownParser { + + private final Parser _markdownParser; + private final HtmlRenderer _markdownRenderer; + + private static Pattern QuoteExtender = Pattern.compile("^([ \t]*>[ \t]*.+)(?=\n[ \t]*[^>])", Pattern.MULTILINE); + + public MarkdownParser() { + List adminExt = Arrays.asList(StrikethroughExtension.create(), AutolinkExtension.create()); + _markdownParser = Parser.builder() + .extensions(adminExt) + .build(); + _markdownRenderer = HtmlRenderer.builder() + .nodeRendererFactory(htmlContext -> new LinkShredder(htmlContext)) + .extensions(adminExt) + .escapeHtml(true) + .sanitizeUrls(true) + .softbreak("
") + .build(); + } + + public String renderMarkdown(String markdown) { + String newMsg = markdown.trim().replaceAll("\n\n\n+", "\n\n\n"); + newMsg = QuoteExtender.matcher(newMsg).replaceAll("$1\n"); + //Escaping underscores so that URLs with lots of underscores (i.e. wiki links) aren't mangled + // Besides, who uses _this_ instead of *this*? + newMsg = newMsg.replace("_", "\\_"); + + //Need to preserve any commands being made + if(!newMsg.startsWith("/")) { + newMsg = _markdownRenderer.render(_markdownParser.parse(newMsg)); + // Prevent quotes with newlines from displaying side-by-side + newMsg = newMsg.replaceAll("
[\n \t]*
", "

"); + //Make all links open in a new tab + newMsg = newMsg.replaceAll("<(a href=\".*?\")>", "<$1 target=\"_blank\">"); + } + + return newMsg; + } + + + //This class is used to ensure that markdown links [like this](https://somewebsite.com) are rendered literally. + // This is to ensure that people are unable to falsely represent links as other links. + private class LinkShredder implements NodeRenderer { + + private final HtmlWriter html; + + LinkShredder(HtmlNodeRendererContext context) { + this.html = context.getWriter(); + } + + @Override + public Set> getNodeTypes() { + // Return the node types we want to use this renderer for. + return new HashSet<>(Arrays.asList( + Link.class, + Image.class + )); + } + + @Override + public void render(Node node) { + if(node instanceof Link link) { + if(link.getTitle() != null) { + html.text(link.getTitle() + ": " + link.getDestination()); + } + else { + if(link.getFirstChild() != null + && link.getFirstChild() instanceof Text text + && !text.getLiteral().equals(link.getDestination())) + { + html.text(text.getLiteral() + ": " + link.getDestination()); + } + else { + html.tag("a", Collections.singletonMap("href", link.getDestination())); + html.text(link.getDestination()); + html.tag("/a"); + } + } + + } + else if(node instanceof Image image){ + html.text(image.getTitle() + ": " + image.getDestination()); + } + } + } + +}