1
0
Fork 0
forked from mc/VTools

反解析md,没测试

This commit is contained in:
Sodium-Aluminate 2023-11-02 05:04:55 +08:00
parent c2ee4fca4f
commit e4c91adac0
2 changed files with 355 additions and 107 deletions

View file

@ -0,0 +1,222 @@
package de.strifel.VTools.listeners;
import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.MessageEntity;
import java.util.*;
import java.util.function.BiFunction;
public class MarkdownString {
private MarkdownString(){
throw new IllegalStateException();
}
/**
* 这里的 format 假设 BiFunction 的输入都是转义过的
*/
private static final Map<MessageEntity.Type, BiFunction<StringBuilder, MessageEntity, StringBuilder>> formats;
static {
Map<MessageEntity.Type, BiFunction<StringBuilder, MessageEntity, StringBuilder>> map = new EnumMap<>(MessageEntity.Type.class);
map.put(MessageEntity.Type.bold, (s, e) -> {
assert e.type() == MessageEntity.Type.bold;
return s.insert(0, "*").append("*");
});
map.put(MessageEntity.Type.strikethrough, (s, e) -> {
assert e.type() == MessageEntity.Type.strikethrough;
return s.insert(0, "~").append("~");
});
map.put(MessageEntity.Type.spoiler, (s, e) -> {
assert e.type() == MessageEntity.Type.spoiler;
return s.insert(0, "||").append("||");
});
map.put(MessageEntity.Type.italic, (s, e) -> {
assert e.type() == MessageEntity.Type.italic;
return s.insert(0, "\r_\r").append("\r_\r"); // todo: 精简一下 到底哪里需要加\r
});
map.put(MessageEntity.Type.underline, (s, e) -> {
assert e.type() == MessageEntity.Type.underline;
return s.insert(0, "\r__\r").append("\r__\r");
});
// https://core.telegram.org/bots/api#formatting-options
// 上面这几个套娃友好自身随意套娃但不能和等宽组合
map.put(MessageEntity.Type.code, (s, e) -> {
assert e.type() == MessageEntity.Type.code;
return s.insert(0, "`").append("`");
});
map.put(MessageEntity.Type.pre, (rawStr, messageEntity) -> {
assert messageEntity.type() == MessageEntity.Type.pre;
rawStr.insert(0, "\n");
String language = messageEntity.language();
if (language != null && language.length() > 0) {
rawStr.insert(0, language);
}
return rawStr.insert(0, "```").append("\n```");
});
// 等宽pre code完全禁止套娃
map.put(MessageEntity.Type.text_link, (s, e) -> {
assert e.type() == MessageEntity.Type.text_link;
return s.insert(0, "[").append("](").append(escapeStr(e.url())).append(")");
});
map.put(MessageEntity.Type.text_mention, (s, e) -> {
assert e.type() == MessageEntity.Type.text_mention;
if (e.user() == null || e.user().id() == null) {
return s; // 没有id时爆炸
}
return s.insert(0, "[").append("](").append("tg://user?id=").append(e.user().id()).append(")");
});
// 链接类不能和链接类套娃但是可以和加粗等套娃友好的组合
formats = Collections.unmodifiableMap(map);
}
private static class StringBlock {
private final String text;
private Map<MessageEntity.Type, MessageEntity> entities;
private final int offset;
StringBlock(String text, int offset) {
this.text = text;
this.offset = offset;
this.entities = new EnumMap<>(MessageEntity.Type.class);
}
private String getMarkdownTextWithoutLink() { // todo: 压缩 *text1**_text2_* *text1_text2_*
// 链接类不能和链接类套娃
assert !((entities.containsKey(MessageEntity.Type.text_link)) && (entities.containsKey(MessageEntity.Type.text_mention)));
// 等宽pre code完全禁止套娃
assert !((entities.containsKey(MessageEntity.Type.pre) || entities.containsKey(MessageEntity.Type.code)) && (entities.size() > 1));
StringBuilder s = new StringBuilder(escapeStr(text));
for (var entry : entities.entrySet()) {
s = formats.get(entry.getKey()).apply(s, entry.getValue()); // 无用赋值...理论上
}
return s.toString();
}
}
public static String markdownString(Message message) {
List<StringBlock> stringBlocks = createStringBlocks(message);
StringBuilder s = new StringBuilder();
for (int i = 0; i < stringBlocks.size(); i++) {
var stringBlock = stringBlocks.get(i);
var entities = stringBlock.entities;
String withoutLink = stringBlock.getMarkdownTextWithoutLink();
if (entities.containsKey(MessageEntity.Type.text_mention) ||
entities.containsKey(MessageEntity.Type.text_link)) {
assert (!entities.containsKey(MessageEntity.Type.text_mention) ||
!entities.containsKey(MessageEntity.Type.text_link));
StringBuilder linkText = new StringBuilder();
MessageEntity entity = entities.containsKey(MessageEntity.Type.text_mention) ?
stringBlock.entities.get(MessageEntity.Type.text_mention) :
stringBlock.entities.get(MessageEntity.Type.text_link);
assert entity.offset() == stringBlock.offset;
int end = entity.length() + entity.offset();
while (true) {
linkText.append(stringBlock.getMarkdownTextWithoutLink());
if (stringBlock.text.length() + stringBlock.offset == end) {
break;
}
assert (stringBlock.text.length() + stringBlock.offset < end);
i++;
stringBlock = stringBlocks.get(i);
}
s.append(formats.get(entity.type()).apply(linkText, entity));
} else {
s.append(withoutLink);
}
}
return s.toString();
}
/**
* @return 一组 StringBlock被完全切开的文本每部分都和相邻部分格式不一样顺序的
*/
private static List<StringBlock> createStringBlocks(Message message) {
List<StringBlock> stringBlocks = new ArrayList<>();
Set<Integer> splitPoints = new HashSet<>();
// 定位切割点
for (MessageEntity entity : message.entities()) {
if (formats.containsKey(entity.type())) {
Integer offset = entity.offset();
Integer length = entity.length();
splitPoints.add(offset);
splitPoints.add(offset + length);
}
}
// 切割文本
String text = message.text();
List<Integer> sortedSplitPoints = new ArrayList<>(splitPoints);
Collections.sort(sortedSplitPoints);
int start = 0;
for (int splitPoint : sortedSplitPoints) {
if (splitPoint == 0) continue;// 跳过第一个空的
stringBlocks.add(new StringBlock(text.substring(start, splitPoint), start));
start = splitPoint;
}
if (start < text.length()) {
stringBlocks.add(new StringBlock(text.substring(start), start));
}
// 给文本重新赋格式MessageEntity
for (MessageEntity entity : message.entities()) {
for (StringBlock stringBlock : stringBlocks) {
int blockStart = stringBlock.offset;
int blockEnd = blockStart + stringBlock.text.length();
int entityStart = entity.offset();
int entityEnd = entityStart + entity.length();
assert (blockStart < blockEnd) && (entityStart < entityEnd);
if (blockStart > entityEnd || blockEnd < entityStart) {
continue;
}
assert (blockStart >= entityStart) && (blockEnd <= entityEnd);
MessageEntity old = stringBlock.entities.put(entity.type(), entity);
assert (old == null);
}
}
return stringBlocks;
}
private static final boolean[] SHOULD_ESCAPE = new boolean[128];
static {
// In all other places characters '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!' must be escaped with the preceding character '\'.
char[] chars = {'_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'};
for (char c : chars) {
SHOULD_ESCAPE[c] = true;
}
}
public static String escapeStr(String s) {
StringBuilder r = new StringBuilder();
for (String character : s.split("")) {
int codePoint = character.codePointAt(0);
if (codePoint < 128 && SHOULD_ESCAPE[codePoint]) {
r.append('\\');
}
r.append(character);
}
return r.toString();
}
}

View file

@ -6,6 +6,7 @@ import com.pengrad.telegrambot.UpdatesListener;
import com.pengrad.telegrambot.model.Message; import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.User; import com.pengrad.telegrambot.model.User;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.EditMessageText; import com.pengrad.telegrambot.request.EditMessageText;
import com.pengrad.telegrambot.request.GetChat; import com.pengrad.telegrambot.request.GetChat;
import com.pengrad.telegrambot.request.SendMessage; import com.pengrad.telegrambot.request.SendMessage;
@ -51,6 +52,9 @@ public class TGBridge {
private long backoffSec = 1L; private long backoffSec = 1L;
private static final String DIVIDER = "----------\n"; private static final String DIVIDER = "----------\n";
/**
* markdown escaped
*/
private String pinNote; private String pinNote;
public TGBridge(@NotNull VTools plugin) { public TGBridge(@NotNull VTools plugin) {
@ -96,50 +100,56 @@ public class TGBridge {
bot.setUpdatesListener(updates -> { bot.setUpdatesListener(updates -> {
backoffSec = 1L; backoffSec = 1L;
for (Update update : updates) { for (Update update : updates) {
try { try {
if (update != null && if (update == null || update.message() == null) break;
update.message() != null && Message message = update.message();
update.message().chat() != null && if (message.chat() == null || message.chat().id() != CHAT_ID || message.from() == null) {
update.message().chat().id() == CHAT_ID && break;
update.message().from() != null }
) { if (message.text() != null && !message.text().isEmpty()) {
if (update.message().text() != null && !update.message().text().isEmpty()) { String msgText = message.text();
String msg = update.message().text(); if (msgText.startsWith("/")) {
if (msg.startsWith("/")) { String[] s = msgText.split("(@[A-Za-z0-9_]bot)?[\t \n]+", 2);
String[] s = msg.split("(@[A-Za-z0-9_]bot)?[\t \n]+", 2);
String command = s[0]; String command = s[0];
@Nullable String arg = s.length == 2 ? s[1] : null; @Nullable String arg = s.length == 2 ? s[1] : null;
switch (command) { switch (command) {
case "/list" -> outbound(genOnlineStatus()); case "/list" -> outbound(genOnlineStatus(), ParseMode.Markdown);
case "/setpin" -> { case "/setpin" -> {
boolean shouldApplyEdit = true; Message replyTo = message.replyToMessage();
boolean needUpdate = false; if (arg == null || arg.length() == 0) {
Message replyTo = update.message().replyToMessage();
if (replyTo == null) { if (replyTo == null) {
if (arg == null || arg.length() == 0)
outbound(""" outbound("""
usage: usage:
use "/setpin" reply a message that from the bot to set that message to pin-message, use "/setpin" reply a message that from the bot to set that message to pin-message,
or use "/setpin <note>" to update current pinned message."""); or use "/setpin <note>" to update current pinned message.""");
} else if (replyTo.from() == null || replyTo.from().id() != BOT_ID || replyTo.messageId() <= 0) { break;
outbound("must reply a message that from the bot (or reply nothing).");
shouldApplyEdit = false;
} else {
readOldPinnedMessage(replyTo);
if (arg == null || arg.length() == 0) {
outbound("done");
}else {
ONLINE_STATUS_MESSAGE_ID = replyTo.messageId();
} }
needUpdate = true; ONLINE_STATUS_MESSAGE_ID = replyTo.messageId();
updateOnlineStatus();
break;
}
if (replyTo != null && (replyTo.from() == null || replyTo.from().id() != BOT_ID || replyTo.messageId() <= 0)) {
outbound("must reply a message that from the bot (or reply nothing).");
break;
} }
if (shouldApplyEdit && arg != null && arg.length() > 0) { String markdownString = MarkdownString.markdownString(message);
outbound("done. old pinned note: \n" + pinNote);
pinNote = arg; String shouldBeCommand = markdownString.substring(0, "/setpin ".length());
needUpdate = true; if (!shouldBeCommand.matches("/setpin[\t \n]")) {
outbound("\"/setpin\" must be plain text.");
break;
} }
if (needUpdate) updateOnlineStatus();
outbound("old pinned note: \n" + pinNote, ParseMode.Markdown);
pinNote = markdownString.substring("/setpin ".length());
if (replyTo != null) {
ONLINE_STATUS_MESSAGE_ID = replyTo.messageId();
}
updateOnlineStatus();
} }
case "/genpin" -> outbound(genPinMessage(), case "/genpin" -> outbound(genPinMessage(),
(sendMessage, sendResponse) -> { (sendMessage, sendResponse) -> {
@ -149,22 +159,21 @@ public class TGBridge {
int messageId = sendResponse.message().messageId(); int messageId = sendResponse.message().messageId();
ONLINE_STATUS_MESSAGE_ID = messageId > 0 ? messageId : ONLINE_STATUS_MESSAGE_ID; ONLINE_STATUS_MESSAGE_ID = messageId > 0 ? messageId : ONLINE_STATUS_MESSAGE_ID;
} }
} }, ParseMode.Markdown
); );
} }
} }
tgInbound(update.message().from(), msg); tgInbound(message.from(), msgText);
} else if (update.message().sticker() != null) { } else if (message.sticker() != null) {
tgInbound(update.message().from(), "[sticker]"); tgInbound(message.from(), "[sticker]");
} else if (update.message().photo() != null) { } else if (message.photo() != null) {
tgInbound(update.message().from(), "[photo]"); tgInbound(message.from(), "[photo]");
} else if (update.message().audio() != null) { } else if (message.audio() != null) {
tgInbound(update.message().from(), "[audio]"); tgInbound(message.from(), "[audio]");
} else if (update.message().voice() != null) { } else if (message.voice() != null) {
tgInbound(update.message().from(), "[voice]"); tgInbound(message.from(), "[voice]");
} else if (update.message().document() != null) { } else if (message.document() != null) {
tgInbound(update.message().from(), "[document]"); tgInbound(message.from(), "[document]");
}
} }
} catch (Exception e) { } catch (Exception e) {
plugin.logger.error("handling update", e); plugin.logger.error("handling update", e);
@ -183,7 +192,6 @@ public class TGBridge {
} }
private boolean getPinnedMessage() { private boolean getPinnedMessage() {
try { try {
GetChatResponse response = bot.execute(new GetChat(CHAT_ID)); GetChatResponse response = bot.execute(new GetChat(CHAT_ID));
Message pinnedMessage = response.chat().pinnedMessage(); Message pinnedMessage = response.chat().pinnedMessage();
@ -200,19 +208,26 @@ public class TGBridge {
private void readOldPinnedMessage(Message message) { private void readOldPinnedMessage(Message message) {
ONLINE_STATUS_MESSAGE_ID = message.messageId(); ONLINE_STATUS_MESSAGE_ID = message.messageId();
String text = message.text(); String markdownText = MarkdownString.markdownString(message);
String[] s = text.split(DIVIDER, 2);
pinNote = s.length == 2 ? s[1] : "(use \"/setpin\" <note> to set note here)"; String[] s = markdownText.split(DIVIDER, 2);
pinNote = (s.length == 2) ?
s[1] :
"\r_\r" + MarkdownString.escapeStr("(use \"/setpin\" <note> to set note here)") + "\r_\r";
} }
/**
* @return markdown escaped str
*/
private String genPinMessage() { private String genPinMessage() {
String onlineStatus = genOnlineStatus(); return (pinNote != null && pinNote.length() != 0) ?
if (pinNote != null && pinNote.length() > 1) { genOnlineStatus() + "\n" + DIVIDER + pinNote :
return onlineStatus + "\n" + DIVIDER + pinNote; genOnlineStatus();
} else {
return onlineStatus;
}
} }
/**
* @return markdown escaped str
*/
private String genOnlineStatus() { private String genOnlineStatus() {
ArrayList<String> out = new ArrayList<>(); ArrayList<String> out = new ArrayList<>();
String fmt = server.getAllPlayers().size() > 1 ? "%d players are currently connected to the proxy." : "%d player is currently connected to the proxy."; String fmt = server.getAllPlayers().size() > 1 ? "%d players are currently connected to the proxy." : "%d player is currently connected to the proxy.";
@ -233,7 +248,7 @@ public class TGBridge {
); );
} }
} }
return String.join("\n", out); return MarkdownString.escapeStr(String.join("\n", out));
} }
protected void tgInbound(User user, String content) { protected void tgInbound(User user, String content) {
@ -248,26 +263,35 @@ public class TGBridge {
} }
} }
protected void outbound(String content) {
outbound(content, null); protected void outbound(String content, ParseMode parseMode) {
outbound(content, (sendMessage, sendResponse) -> {
if (!sendResponse.isOk()) {
plugin.logger.error(String.format("sendMessage error %d: %s", sendResponse.errorCode(), sendResponse.description()));
}
}, parseMode);
} }
protected void outbound(String content, @Nullable BiConsumer<SendMessage, SendResponse> onResponse) { protected void outbound(String content) {outbound(content, (ParseMode) null);}
protected void outbound(String content, @NotNull BiConsumer<SendMessage, SendResponse> onResponse) {outbound(content, onResponse, null);}
protected void outbound(String content, @NotNull BiConsumer<SendMessage, SendResponse> onResponse, ParseMode parseMode) {
if (bot == null) return; if (bot == null) return;
if (content.length() > 4000) { if (content.length() > 4000) {
content = content.substring(0, 4000); content = content.substring(0, 4000);
} }
bot.execute(new SendMessage(CHAT_ID, content), new Callback<SendMessage, SendResponse>() {
SendMessage sendMessage = new SendMessage(CHAT_ID, content);
if (parseMode != null) {
sendMessage.parseMode(parseMode);
}
bot.execute(sendMessage, new Callback<SendMessage, SendResponse>() {
@Override @Override
public void onResponse(SendMessage sendMessage, SendResponse sendResponse) { public void onResponse(SendMessage sendMessage, SendResponse sendResponse) {
if (onResponse == null) {
if (!sendResponse.isOk()) {
plugin.logger.error(String.format("sendMessage error %d: %s", sendResponse.errorCode(), sendResponse.description()));
}
} else {
onResponse.accept(sendMessage, sendResponse); onResponse.accept(sendMessage, sendResponse);
} }
}
@Override @Override
public void onFailure(SendMessage sendMessage, IOException e) { public void onFailure(SendMessage sendMessage, IOException e) {
@ -299,6 +323,7 @@ public class TGBridge {
} }
private String lastDisconnect = ""; private String lastDisconnect = "";
@Subscribe @Subscribe
public void onDisconnect(DisconnectEvent event) { public void onDisconnect(DisconnectEvent event) {
if (!event.getPlayer().hasPermission("vtools.globalchat.bypassbridge.join")) { if (!event.getPlayer().hasPermission("vtools.globalchat.bypassbridge.join")) {
@ -318,10 +343,10 @@ public class TGBridge {
} }
private boolean PROXY_SHUT_DOWN = false; private boolean PROXY_SHUT_DOWN = false;
private static class UpdateRequest {} private static class UpdateRequest {
}
private LinkedBlockingQueue<UpdateRequest> updateRequests = new LinkedBlockingQueue<>(); private LinkedBlockingQueue<UpdateRequest> updateRequests = new LinkedBlockingQueue<>();
@ -381,16 +406,16 @@ public class TGBridge {
} }
protected boolean setOnlineStatusNotAvailable() { protected boolean setOnlineStatusNotAvailable() {
return editOnlineStatusMessage("(proxy already shutdown)"); return editOnlineStatusMessage("proxy already shutdown");
} }
protected boolean editOnlineStatusMessage(String text) { protected boolean editOnlineStatusMessage(String markdownText) {
if (ONLINE_STATUS_MESSAGE_ID < 1) { if (ONLINE_STATUS_MESSAGE_ID < 1) {
return true; return true;
} }
BaseResponse response; BaseResponse response;
try { try {
response = bot.execute(new EditMessageText(CHAT_ID, ONLINE_STATUS_MESSAGE_ID, text)); response = bot.execute(new EditMessageText(CHAT_ID, ONLINE_STATUS_MESSAGE_ID, markdownText).parseMode(ParseMode.Markdown));
} catch (RuntimeException e) {return false;} } catch (RuntimeException e) {return false;}
return response != null && response.isOk(); return response != null && response.isOk();
} }
@ -440,6 +465,7 @@ public class TGBridge {
time = 0; time = 0;
text = new StringBuilder(); text = new StringBuilder();
} //dummy } //dummy
private void addLines(List<String> messages) { private void addLines(List<String> messages) {
for (String message : messages) { for (String message : messages) {
text.append('\n').append(message); text.append('\n').append(message);