Extracting markdown parsing out of the chat request handler and putting it in its own class so it can be used by other processes

This commit is contained in:
Christian 'ketura' McCarty
2024-11-30 18:36:42 -06:00
parent 8d4e146fe6
commit 0e0dc5c148
5 changed files with 127 additions and 111 deletions

View File

@@ -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<Type, Object> context, LongPollingSystem longPollingSystem) {
super(context);
_chatServer = extractObject(context, ChatServer.class);
this.longPollingSystem = longPollingSystem;
List<Extension> 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("<br />")
.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("</blockquote>[\n \t]*<blockquote>", "</blockquote><br /><blockquote>");
//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<Class<? extends Node>> 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;

View File

@@ -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));

View File

@@ -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<Type, Object> objectMap) {

View File

@@ -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<String, ChatRoomMediator> _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<String> 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) {

View File

@@ -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<Extension> 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("<br />")
.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("</blockquote>[\n \t]*<blockquote>", "</blockquote><br /><blockquote>");
//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<Class<? extends Node>> 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());
}
}
}
}