[Add] BotCode 增加用于操作机器人功能代码的工具类;

[Add] BotEventHandler, MessageEvent 将CQPluginMain消息处理部分迁移至BotEventHandler;
[Add] SpringCQMessageEvent, MiraiMessageEvent 增加两个消息事件实现;
[Add] MiraiMain 增加对 Mirai 框架的支持;
[Add] Main 增加 Mirai 框架的支持;
This commit is contained in:
LamGC 2020-04-11 12:27:24 +08:00
parent 7d843c3f77
commit bc6aaa49a0
7 changed files with 668 additions and 7 deletions

View File

@ -7,6 +7,8 @@ import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import net.lamgc.cgj.bot.CQConfig;
import net.lamgc.cgj.bot.MiraiMain;
import net.lamgc.cgj.pixiv.*;
import net.lamgc.cgj.proxy.PixivAccessProxyServer;
import net.lamgc.cgj.proxy.PixivLoginProxyServer;
@ -49,12 +51,6 @@ public class Main {
public static HttpHost proxy;
static {
if(!storeDir.exists() && !storeDir.mkdirs()) {
log.error("创建文件夹失败!");
}
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
ArgumentsProperties argsProp = new ArgumentsProperties(args);
if(argsProp.containsKey("proxy")) {
@ -65,11 +61,16 @@ public class Main {
proxy = null;
}
if(!storeDir.exists() && !storeDir.mkdirs()) {
log.error("创建文件夹失败!");
}
// TODO: 需要修改参数名了, 大概改成类似于 workerDir这样的吧
if(argsProp.containsKey("cqRootDir")) {
log.info("cqRootDir: {}", argsProp.getValue("cqRootDir"));
System.setProperty("cgj.cqRootDir", argsProp.getValue("cqRootDir"));
} else {
log.info("未设置cqRootDir, 当前运行目录将作为酷Q机器人所在目录.");
log.warn("未设置cqRootDir, 当前运行目录将作为酷Q机器人所在目录.");
System.setProperty("cgj.cqRootDir", "./");
}
@ -97,6 +98,11 @@ public class Main {
ArgumentsRunner.run(Main.class, args);
}
@Command
public static void botMode(@Argument(name = "args", force = false) String argsStr) {
new MiraiMain().init();
}
@Command
public static void pluginMode(@Argument(name = "args", force = false) String argsStr) {
if(!System.getProperty("cgj.cqRootDir").endsWith("\\") && !System.getProperty("cgj.cqRootDir").endsWith("/")) {

View File

@ -0,0 +1,158 @@
package net.lamgc.cgj.bot;
import com.google.common.base.Strings;
import java.util.*;
import java.util.regex.Pattern;
public class BotCode {
public static BotCode build(String platformName, String functionName) {
return build(platformName, functionName, null);
}
public static BotCode build(String platformName, String functionName, Map<String, String> parameter) {
if(Strings.isNullOrEmpty(platformName)) {
throw new IllegalArgumentException("platformName is Null or Empty.");
} else if(Strings.isNullOrEmpty(functionName)) {
throw new IllegalArgumentException("functionName is Null or Empty.");
}
return new BotCode(platformName, functionName, parameter);
}
private final static Pattern codePattern = Pattern.compile("\\[.*?:.*?]");
public static BotCode parse(String str) {
if (!codePattern.matcher(str).matches()) {
throw new IllegalArgumentException("invalid string input: " + str);
}
String text = str.substring(1, str.length() - 1);
String[] texts = text.split(",");
if(texts.length <= 0) {
throw new IllegalArgumentException("invalid string input: " + str);
}
String[] keys = texts[0].split(":", 2);
if(keys.length != 2) {
throw new IllegalArgumentException("invalid string input: " + str);
}
if(Strings.isNullOrEmpty(keys[0]) || Strings.isNullOrEmpty(keys[1])) {
throw new IllegalArgumentException("invalid string input: " + str);
}
HashMap<String, String> param = new HashMap<>(texts.length - 1);
for (int i = 1; i < texts.length; i++) {
String[] items = texts[i].split("=");
if(items.length != 2) {
continue;
}
param.put(items[0].trim(), items[1]);
}
return new BotCode(keys[0], keys[1], param);
}
private String platformName;
private String functionName;
private Hashtable<String, String> parameter = new Hashtable<>();
/**
* 构造一个机器功能码
* @param platformName 平台代码
* @param functionName 功能名
* @param parameter 参数Map
*/
private BotCode(String platformName, String functionName, Map<String, String> parameter) {
this.platformName = platformName;
this.functionName = functionName;
if(parameter != null && !parameter.isEmpty()) {
this.parameter.putAll(parameter);
}
}
/**
* 设置平台代码
* @param platformName 欲设置的平台代码
*/
public void setPlatformName(String platformName) {
this.platformName = platformName;
}
/**
* 获取平台代码
* @return 当前设置的平台代码
*/
public String getPlatformName() {
return platformName;
}
/**
* 设置功能名
* @param functionName 欲设置的新功能名
*/
public void setFunctionName(String functionName) {
this.functionName = functionName;
}
/**
* 获取功能名
* @return 返回当前设置的功能名
*/
public String getFunctionName() {
return functionName;
}
/**
* 添加参数
* @param key 参数键
* @param value 参数值
*/
public void addParameter(String key, String value) {
parameter.put(key.trim(), value);
}
/**
* 检查一个参数项是否存在
* @param key 欲查询其存在的参数项所属键
* @return 返回true则参数项存在
*/
public boolean containsParameter(String key) {
return parameter.containsKey(key.trim());
}
/**
* 获取参数项的值
* @param key 欲获取参数值的参数项所属键
* @return 返回参数项的参数值
*/
public String getParameter(String key) {
return parameter.get(key.trim());
}
/**
* 获取所有参数项的参数键
* @return 返回存储了所有参数键的Set对象
*/
public Set<String> parameterKeys() {
return new HashSet<>(parameter.keySet());
}
/**
* 将BotCode对象转为功能代码文本
* 格式:
* <pre>[Platform:Function, parameter...]</pre>
* @return 功能代码文本
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder("[" + platformName + ":" + functionName);
if(!parameter.isEmpty()) {
builder.append(", ");
parameter.forEach((key, value) -> builder.append(key).append("=").append(value).append(", "));
builder.replace(builder.length() - 2, builder.length(), "");
}
return builder.append("]").toString();
}
}

View File

@ -0,0 +1,52 @@
package net.lamgc.cgj.bot;
import net.lamgc.cgj.bot.event.BotEventHandler;
import net.lamgc.cgj.bot.event.MiraiMessageEvent;
import net.mamoe.mirai.Bot;
import net.mamoe.mirai.japt.Events;
import net.mamoe.mirai.message.FriendMessage;
import net.mamoe.mirai.message.GroupMessage;
import net.mamoe.mirai.qqandroid.QQAndroid;
import net.mamoe.mirai.utils.BotConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.Properties;
public class MiraiMain implements Closeable {
private final Logger log = LoggerFactory.getLogger(this.toString());
private Bot bot;
private final static Properties botProperties = new Properties();
public void init() {
try {
Class.forName(BotEventHandler.class.getName());
} catch (ClassNotFoundException e) {
log.error("加载BotEventHandler时发生异常", e);
return;
}
File botPropFile = new File("./bot.properties");
try (Reader reader = new BufferedReader(new FileReader(botPropFile))) {
botProperties.load(reader);
} catch (IOException e) {
log.error("机器人配置文件读取失败!", e);
return;
}
bot = QQAndroid.INSTANCE.newBot(Long.parseLong(botProperties.getProperty("bot.qq", "0")), botProperties.getProperty("bot.password", ""), new BotConfiguration());
Events.subscribeAlways(GroupMessage.class, (msg) -> BotEventHandler.executor.executor(new MiraiMessageEvent(msg)));
Events.subscribeAlways(FriendMessage.class, (msg) -> BotEventHandler.executor.executor(new MiraiMessageEvent(msg)));
bot.login();
bot.join();
}
public void close() {
bot.close(null);
}
}

View File

@ -0,0 +1,174 @@
package net.lamgc.cgj.bot.event;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import net.lamgc.cgj.bot.BotAdminCommandProcess;
import net.lamgc.cgj.bot.BotCommandProcess;
import net.lamgc.cgj.util.DateParser;
import net.lamgc.cgj.util.PagesQualityParser;
import net.lamgc.utils.base.runner.ArgumentsRunner;
import net.lamgc.utils.base.runner.ArgumentsRunnerConfig;
import net.lamgc.utils.base.runner.exception.DeveloperRunnerException;
import net.lamgc.utils.base.runner.exception.NoSuchCommandException;
import net.lamgc.utils.base.runner.exception.ParameterNoFoundException;
import net.lamgc.utils.event.EventExecutor;
import net.lamgc.utils.event.EventHandler;
import net.lamgc.utils.event.EventObject;
import net.lamgc.utils.event.EventUncaughtExceptionHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.JedisPool;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class BotEventHandler implements EventHandler {
public final static String COMMAND_PREFIX = ".cgj";
private final ArgumentsRunner processRunner;
private final ArgumentsRunner adminRunner;
private final Logger log = LoggerFactory.getLogger("BotEventHandler@" + Integer.toHexString(this.hashCode()));
/**
* 所有缓存共用的JedisPool
*/
private final static URI redisServerUri = URI.create("redis://" + System.getProperty("cgj.redisAddress"));
public final static JedisPool redisServer = new JedisPool(redisServerUri.getHost(), redisServerUri.getPort() == -1 ? 6379 : redisServerUri.getPort());
/**
* 消息事件执行器
*/
public final static EventExecutor executor = new EventExecutor(new ThreadPoolExecutor(
(int) Math.ceil(Runtime.getRuntime().availableProcessors() / 2F),
Runtime.getRuntime().availableProcessors(),
30L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1536),
new ThreadFactoryBuilder()
.setNameFormat("CommandProcess-%d")
.build()
));
static {
executor.setEventUncaughtExceptionHandler(new EventUncaughtExceptionHandler() {
private final Logger log = LoggerFactory.getLogger("EventUncaughtExceptionHandler");
@Override
public void exceptionHandler(Thread executeThread, EventHandler handler, Method handlerMethod, EventObject event, Throwable cause) {
log.error("发生未捕获异常:\nThread:{}, EventHandler: {}, HandlerMethod: {}, EventObject: {}\n{}",
executeThread.getName(),
handler.toString(),
handlerMethod.getName(),
event.toString(),
Throwables.getStackTraceAsString(cause));
}
});
try {
executor.addHandler(new BotEventHandler());
executor.addHandler(new TestEventHandler());
} catch (IllegalAccessException e) {
LoggerFactory.getLogger("BotEventHandler@Static").error("添加Handler时发生异常", e);
}
}
private BotEventHandler() {
ArgumentsRunnerConfig runnerConfig = new ArgumentsRunnerConfig();
runnerConfig.setUseDefaultValueInsteadOfException(true);
runnerConfig.setCommandIgnoreCase(true);
runnerConfig.addStringParameterParser(new DateParser(new SimpleDateFormat("yyyy-MM-dd")));
runnerConfig.addStringParameterParser(new PagesQualityParser());
log.debug("DateParser添加情况: {}", runnerConfig.hasStringParameterParser(Date.class));
processRunner = new ArgumentsRunner(BotCommandProcess.class, runnerConfig);
adminRunner = new ArgumentsRunner(BotAdminCommandProcess.class, runnerConfig);
BotCommandProcess.initialize();
}
public void processMessage(MessageEvent event) {
String msg = event.getMessage();
if(!match(msg)) {
return;
}
Pattern pattern = Pattern.compile("/\\s*(\".+?\"|[^:\\s])+((\\s*:\\s*(\".+?\"|[^\\s])+)|)|(\".+?\"|[^\"\\s])+");
Matcher matcher = pattern.matcher(Strings.nullToEmpty(msg));
ArrayList<String> argsList = new ArrayList<>();
while (matcher.find()) {
String arg = matcher.group();
int startIndex = 0;
int endIndex = arg.length();
if(arg.startsWith("\"")) {
while(arg.indexOf("\"", startIndex) == startIndex) {
startIndex++;
}
}
if(arg.endsWith("\"")) {
while(arg.charAt(endIndex - 1) == '\"') {
endIndex--;
}
}
argsList.add(arg.substring(startIndex, endIndex));
}
String[] args = new String[argsList.size()];
argsList.toArray(args);
log.debug("传入参数: {}", Arrays.toString(args));
log.info("正在处理命令...");
long time = System.currentTimeMillis();
Object result;
try {
if(msg.toLowerCase().startsWith(COMMAND_PREFIX + "admin")) {
if(!String.valueOf(event.getFromQQ()).equals(BotCommandProcess.globalProp.getProperty("admin.adminId"))) {
event.sendMessage("你没有执行该命令的权限!");
return;
} else {
result = adminRunner.run(new BotAdminCommandProcess(), args.length <= 1 ? new String[0] : Arrays.copyOfRange(args, 1, args.length));
}
} else {
result = processRunner.run(args.length <= 1 ? new String[0] : Arrays.copyOfRange(args, 1, args.length));
}
} catch(NoSuchCommandException e) {
result = "没有这个命令!请使用“.cgj”查看帮助说明";
} catch(ParameterNoFoundException e) {
result = "命令缺少参数: " + e.getParameterName();
} catch(DeveloperRunnerException e) {
log.error("执行命令时发生异常", e);
result = "命令执行时发生错误,无法完成!";
}
log.info("命令处理完成.(耗时: {}ms)", System.currentTimeMillis() - time);
if(Objects.requireNonNull(result) instanceof String) {
try {
event.sendMessage((String) result);
} catch (Exception e) {
log.error("发送消息时发生异常", e);
}
}
log.info("命令反馈完成.(耗时: {}ms)", System.currentTimeMillis() - time);
}
/**
* 检查消息是否需要提交
* @param message 要检查的消息
* @return 如果为true则提交
*/
public static boolean match(String message) {
return message.startsWith(COMMAND_PREFIX);
}
}

View File

@ -0,0 +1,33 @@
package net.lamgc.cgj.bot.event;
import net.lamgc.utils.event.EventObject;
public abstract class MessageEvent implements EventObject {
private final long fromGroup;
private final long fromQQ;
private final String message;
public MessageEvent(long fromGroup, long fromQQ, String message) {
this.fromGroup = fromGroup;
this.fromQQ = fromQQ;
this.message = message;
}
public abstract int sendMessage(final String message);
public abstract Object getRawMessage();
public long getFromGroup() {
return fromGroup;
}
public long getFromQQ() {
return fromQQ;
}
public String getMessage() {
return message;
}
}

View File

@ -0,0 +1,184 @@
package net.lamgc.cgj.bot.event;
import com.google.common.base.Strings;
import net.lamgc.cgj.bot.BotCode;
import net.lamgc.cgj.bot.cache.CacheStore;
import net.lamgc.cgj.bot.cache.HotDataCacheStore;
import net.lamgc.cgj.bot.cache.LocalHashCacheStore;
import net.lamgc.cgj.bot.cache.StringRedisCacheStore;
import net.mamoe.mirai.message.ContactMessage;
import net.mamoe.mirai.message.FriendMessage;
import net.mamoe.mirai.message.GroupMessage;
import net.mamoe.mirai.message.data.CombinedMessage;
import net.mamoe.mirai.message.data.Image;
import net.mamoe.mirai.message.data.Message;
import net.mamoe.mirai.message.data.MessageUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MiraiMessageEvent extends MessageEvent {
private final ContactMessage messageObject;
private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName() + "@" + Integer.toHexString(this.hashCode()));
private final static CacheStore<String> imageIdCache = new HotDataCacheStore<>(
new StringRedisCacheStore(BotEventHandler.redisServer, "mirai.imageId"),
new LocalHashCacheStore<>(),
5400000, 1800000);
public MiraiMessageEvent(ContactMessage message) {
super(message instanceof GroupMessage ? ((GroupMessage) message).getGroup().getId() : 0,
message.getSender().getId(), message.getMessage().toString());
this.messageObject = Objects.requireNonNull(message);
}
@Override
public int sendMessage(final String message) {
log.debug("处理前的消息内容:\n{}", message);
Message msgBody = processMessage(Objects.requireNonNull(message));
log.debug("处理后的消息内容(可能出现乱序的情况, 但实际上顺序是没问题的):\n{}", msgBody);
if(getFromGroup() == 0) {
FriendMessage msgObject = (FriendMessage) messageObject;
//FIXME(LamGC, 2020.04.10): 当前 Mirai 不支持私聊长文本, 所以发生异常是正常情况...
msgObject.getSender().sendMessage(msgBody);
} else {
GroupMessage msgObject = (GroupMessage) messageObject;
msgObject.getGroup().sendMessage(msgBody);
}
return 0;
}
@Override
public Object getRawMessage() {
return messageObject;
}
private final static Pattern cqCodePattern = Pattern.compile("\\[.*?:.*?]");
private Message processMessage(final String message) {
Matcher matcher = cqCodePattern.matcher(message);
ArrayList<String> cqCode = new ArrayList<>();
while (matcher.find()) {
cqCode.add(matcher.group());
}
String[] texts = message
.replaceAll("&", "&38")
.replaceAll("\\{", "&" + Character.getNumericValue('{'))
.replaceAll(cqCodePattern.pattern(), "|{BotCode}|")
.replaceAll("&" + Character.getNumericValue('{'), "{")
.replaceAll("&38", "&")
.split("\\|");
CombinedMessage chain = MessageUtils.newChain().plus("");
int codeIndex = 0;
for(String text : texts) {
if(text.equals("{BotCode}")) {
BotCode code;
try {
code = BotCode.parse(cqCode.get(codeIndex++));
} catch(IllegalArgumentException e) {
log.warn("解析待发送消息内的BotCode时发生异常, 请检查错误格式BotCode的来源并尽快排错!", e);
continue;
}
chain = chain.plus(processBotCode(code));
} else {
chain = chain.plus(text);
}
}
return chain;
}
private Message processBotCode(BotCode code) {
switch(code.getFunctionName().toLowerCase()) {
case "image":
if(code.containsParameter("id")) {
return MessageUtils.newImage(code.getParameter("id"));
} else if(code.containsParameter("absolutePath")) {
return uploadImage(code);
} else {
return MessageUtils.newChain("(参数不存在)");
}
default:
log.warn("解析到不支持的BotCode: {}", code);
return MessageUtils.newChain("(不支持的BotCode)");
}
}
private Image uploadImage(BotCode code) {
log.debug("传入BotCode信息:\n{}", code);
String absolutePath = code.getParameter("absolutePath");
if(Strings.isNullOrEmpty(absolutePath)) {
throw new IllegalArgumentException("BotCode does not contain the absolutePath parameter");
}
String imageName = code.getParameter("imageName");
if(!Strings.isNullOrEmpty(imageName)) {
Image image = null;
imageName = (getMessageSource() + "." + imageName).intern();
if(!imageIdCache.exists(imageName) ||
Strings.nullToEmpty(code.getParameter("updateCache")).equalsIgnoreCase("true")) {
synchronized (imageName) {
if(!imageIdCache.exists(imageName) ||
Strings.nullToEmpty(code.getParameter("updateCache")) .equalsIgnoreCase("true")) {
log.debug("imageName [{}] 缓存失效或强制更新, 正在更新缓存...", imageName);
image = uploadImage0(new File(absolutePath));
if(Objects.isNull(image)) {
return null;
}
String cacheExpireAt;
long expireTime = 0;
if(!Strings.isNullOrEmpty(cacheExpireAt = code.getParameter("cacheExpireAt"))) {
try {
expireTime = Integer.parseInt(cacheExpireAt);
} catch (NumberFormatException e) {
log.warn("BotCode中的cacheExpireAt参数无效: {}", cacheExpireAt);
}
}
imageIdCache.update(imageName, image.getImageId(), expireTime);
log.info("imageName [{}] 缓存更新完成.(有效时间: {})", imageName, expireTime);
} else {
log.debug("ImageName: [{}] 缓存命中.", imageName);
}
}
} else {
log.debug("ImageName: [{}] 缓存命中.", imageName);
}
String cache = imageIdCache.getCache(imageName);
return image != null ? image : MessageUtils.newImage(cache);
} else {
log.debug("未设置imageName, 无法使用缓存.");
return uploadImage0(new File(absolutePath));
}
}
private Image uploadImage0(File imageFile) {
if(messageObject instanceof FriendMessage) {
return messageObject.getSender().uploadImage(imageFile);
} else if(messageObject instanceof GroupMessage) {
return ((GroupMessage) messageObject).getGroup().uploadImage(imageFile);
} else {
log.warn("未知的ContactMessage类型: " + messageObject.toString());
return null;
}
}
private String getMessageSource() {
if(messageObject instanceof FriendMessage) {
return "Private";
} else if(messageObject instanceof GroupMessage) {
return "Group";
} else {
log.warn("未知的ContactMessage类型: " + messageObject.toString());
return "Unknown";
}
}
}

View File

@ -0,0 +1,54 @@
package net.lamgc.cgj.bot.event;
import net.lz1998.cq.event.message.CQDiscussMessageEvent;
import net.lz1998.cq.event.message.CQGroupMessageEvent;
import net.lz1998.cq.event.message.CQMessageEvent;
import net.lz1998.cq.robot.CoolQ;
import java.util.Objects;
public class SpringCQMessageEvent extends MessageEvent {
private final static int TYPE_PRIVATE = 0;
private final static int TYPE_GROUP = 1;
private final static int TYPE_DISCUSS = 2;
private final CoolQ cq;
private final int type;
private final CQMessageEvent messageEvent;
public SpringCQMessageEvent(CoolQ cq, CQMessageEvent messageEvent) {
super(messageEvent instanceof CQGroupMessageEvent ? (
(CQGroupMessageEvent) messageEvent).getGroupId() :
messageEvent instanceof CQDiscussMessageEvent ?
((CQDiscussMessageEvent) messageEvent).getDiscussId() : 0,
messageEvent.getUserId(), messageEvent.getMessage());
this.cq = Objects.requireNonNull(cq);
if(messageEvent instanceof CQGroupMessageEvent) {
type = TYPE_GROUP;
} else if (messageEvent instanceof CQDiscussMessageEvent) {
type = TYPE_DISCUSS;
} else {
type = TYPE_PRIVATE;
}
this.messageEvent = messageEvent;
}
@Override
public int sendMessage(final String message) {
switch(type) {
case TYPE_PRIVATE:
return cq.sendPrivateMsg(getFromQQ(), message, false).getData().getMessageId();
case TYPE_GROUP:
case TYPE_DISCUSS:
return cq.sendGroupMsg(getFromGroup(), message, false).getData().getMessageId();
default:
return -1;
}
}
@Override
public Object getRawMessage() {
return messageEvent;
}
}