*不严谨判定:{@code available}表示是否可用访问API,但不能判定{@code refresh_token}是否可再次刷新。
71 | * 但事实上,{@code refresh_token}的{@code expire_in}并不准确,应该尽可能的刷新。
72 | *
73 | * 而对于能够即时刷新的定时程序来说,可以认为{@code available}等价于{@code refreshable}
74 | *
75 | * @return Token是否可用
76 | */
77 | public boolean checkAvailable() {
78 | try(Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/api/Message/GetUnreadCount",
79 | Map.of("Authorization", getBearerToken()))) {
80 | available = response!=null && response.code()==200;
81 | }
82 | return !(!available | accessToken==null | refreshToken==null);
83 | // return available;
84 | }
85 |
86 | //如果是默认Token(公共token)
87 | @Deprecated
88 | public boolean isDefault() {
89 | return userId==0;
90 | }
91 |
92 | public boolean refresh() {
93 | if(!available) return false;
94 |
95 | try {
96 | Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/token",
97 | Map.of("content-type", "application/x-www-form-urlencoded"),
98 | Map.of("client_type", "qrcode", "grant_type", "refresh_token", "refresh_token", refreshToken));
99 | JsonObject json;
100 | if(response!=null && response.body()!=null) {
101 | json = JsonParser.parseString(response.body().string()).getAsJsonObject();
102 | response.close();
103 | if(response.code()!=200) {
104 | available = false;
105 | throw new IOException("code:" + response.code() + " id:" + userId + " msg:" + response.message());
106 | } else {
107 | accessToken = json.get("access_token").getAsString();
108 | refreshToken = json.get("refresh_token").getAsString();
109 | recTime = System.currentTimeMillis();
110 | return true;
111 | }
112 | }
113 | } catch(IOException e) {
114 | System.out.println("# refreshTokenHttp执行bug辣!");
115 | e.printStackTrace();
116 | return false;
117 | }
118 | return false;
119 | }
120 |
121 | @Override
122 | public String toString() {
123 | return ("""
124 | {
125 | "userId"="%s",
126 | "accessToken"="%s",
127 | "refreshToken"="%s",
128 | "recTime"=%d
129 | "desc"="当前token时长为%.3f天。Token拥有账号的所有控制权,请务必保管好token以免泄露,你可以发送"退出登录"来删除当前会话token"
130 | }
131 | """)
132 | .formatted(userId, accessToken, refreshToken, recTime, (float) (System.currentTimeMillis() - recTime) / 86400_000);
133 | }
134 |
135 | public void forceAccessible() {
136 | this.available = true;
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/main/java/com/dancecube/token/TokenBuilder.java:
--------------------------------------------------------------------------------
1 | package com.dancecube.token;
2 |
3 | import com.google.gson.Gson;
4 | import com.google.gson.GsonBuilder;
5 | import com.google.gson.JsonObject;
6 | import com.google.gson.JsonParser;
7 | import com.google.gson.reflect.TypeToken;
8 | import com.mirai.task.RefreshTokenJob;
9 | import com.tools.HttpUtil;
10 | import okhttp3.Call;
11 | import okhttp3.Response;
12 | import org.junit.jupiter.api.Test;
13 | import org.quartz.*;
14 | import org.quartz.impl.StdSchedulerFactory;
15 |
16 | import java.io.*;
17 | import java.lang.reflect.Type;
18 | import java.net.URLEncoder;
19 | import java.nio.charset.Charset;
20 | import java.nio.charset.StandardCharsets;
21 | import java.nio.file.Files;
22 | import java.nio.file.StandardOpenOption;
23 | import java.util.ArrayList;
24 | import java.util.HashMap;
25 | import java.util.Map;
26 |
27 | import static com.mirai.config.AbstractConfig.configPath;
28 |
29 | public final class TokenBuilder {
30 | //公用 ids
31 | private static ArrayList ids = initIds();
32 |
33 | //公用 pointer
34 | private static int pointer = 0;
35 |
36 | //临时 index
37 | private final int index;
38 |
39 | //临时个人 id,多线程不会阻塞
40 | private final String id;
41 |
42 |
43 | public static ArrayList initIds() {
44 | try {
45 | File tokenIdsFile = new File(configPath + "TokenIds.json");
46 | if(!tokenIdsFile.exists()) {
47 | tokenIdsFile.createNewFile();
48 | Files.writeString(tokenIdsFile.toPath(), "[]", StandardOpenOption.WRITE);
49 | }
50 | ArrayList strings = new ArrayList<>();
51 | JsonParser.parseString(Files.readString(tokenIdsFile.toPath())).getAsJsonArray().forEach(
52 | element -> strings.add(element.getAsString())
53 | );
54 |
55 | if(strings.isEmpty()) throw new RuntimeException("# id缺失,请手动补充!");
56 |
57 | System.out.printf("# id缓存成功,共%d项%n", strings.size());
58 | return strings;
59 | } catch(FileNotFoundException e) {
60 | throw new RuntimeException("TokenIds.json 文件未找到,请重新配置");
61 | } catch(IOException e) {
62 | throw new RuntimeException(e);
63 | }
64 | }
65 |
66 | public TokenBuilder() {
67 | this.index = pointer;
68 | this.id = getId();
69 | }
70 |
71 | public static ArrayList updateIds() {
72 | TokenBuilder.ids = initIds();
73 | return ids;
74 | }
75 |
76 | public static int getSize() {
77 | return ids.size();
78 | }
79 |
80 | private static String getId() {
81 | //ID会一段时间释放,需要换用ID
82 | if(pointer>getSize() - 1) pointer = 0;
83 | return ids.get(pointer++);
84 | }
85 |
86 | public String getQrcodeUrl() {
87 | return getQrcodeUrl(id);
88 | }
89 |
90 | private String getQrcodeUrl(String id) {
91 | //对于含有'+' '/'等符号的TokenId会自动url编码
92 | id = URLEncoder.encode(id, StandardCharsets.UTF_8);
93 |
94 | String string = "";
95 | try {
96 | Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/api/Common/GetQrCode?id=" + id);
97 | if(response!=null && response.body()!=null) {
98 | string = response.body().string();
99 | response.close(); // 释放
100 | return JsonParser.parseString(string).getAsJsonObject().get("QrcodeUrl").getAsString();
101 | }
102 | return "";
103 | } catch(IOException e) {
104 | throw new RuntimeException(e);
105 | } catch(NullPointerException e) {
106 | System.out.println(string);
107 | ids.remove(index);
108 | System.out.println("# ID:" + id + " 不可用已删除,还剩下" + getSize() + "条");
109 | throw new RuntimeException(e);
110 | }
111 | }
112 |
113 | public String getNewID() {
114 | try {
115 | Response response = HttpUtil.httpApi("https://dancedemo.shenghuayule.com/Dance/api/Common/GetQrCode");
116 |
117 | String string;
118 | if(response!=null && response.body()!=null) {
119 | string = response.body().string();
120 | if(response.code()==400) return null;
121 | response.close(); // 释放
122 | JsonObject jsonObject = JsonParser.parseString(string).getAsJsonObject();
123 | System.out.println(jsonObject);
124 | return jsonObject.get("ID").getAsString();
125 | }
126 | return "";
127 | } catch(IOException e) {
128 | throw new RuntimeException(e);
129 | }
130 | }
131 |
132 | @Test
133 | public void testID() throws InterruptedException {
134 | ArrayList strings = new ArrayList<>();
135 | for(int i = 0; i<100; i++) {
136 | String id = getNewID();
137 | Thread.sleep(100);
138 | if(id.isBlank()) continue;
139 | strings.add("\"" + id + "\"");
140 | }
141 | System.out.println(strings);
142 | System.out.println("共 " + strings.size());
143 | }
144 |
145 | public Token getToken() {
146 | return getToken(id);
147 | }
148 |
149 | public Token getToken(String id) {
150 | long curTime = System.currentTimeMillis();
151 | Call call = HttpUtil.httpApiCall("https://dancedemo.shenghuayule.com/Dance/token",
152 | Map.of("content-type", "application/x-www-form-urlencoded"),
153 | Map.of("client_type", "qrcode",
154 | "grant_type", "client_credentials",
155 | "client_id", URLEncoder.encode(id, Charset.defaultCharset())));
156 | Response response;
157 | //五分钟计时
158 | long wait = System.currentTimeMillis();
159 | while(System.currentTimeMillis() - curTime<300_000) { // 5min超时
160 | if(System.currentTimeMillis() - wait<4000) continue; //等待4s时间(防止高频)
161 | wait = System.currentTimeMillis(); //重新赋值等待时间
162 |
163 | try {
164 | //call不能重复请求
165 | response = call.clone().execute();
166 | //未登录为 400 登录为 200
167 | if(response.body()!=null && response.code()==200) {
168 | JsonObject json = JsonParser.parseString(response.body().string()).getAsJsonObject();
169 | return new Token(json.get("userId").getAsInt(),
170 | json.get("access_token").getAsString(),
171 | json.get("refresh_token").getAsString(),
172 | curTime);
173 | }
174 | response.close(); // 关闭释放
175 | } catch(IOException e) {
176 | System.out.println("# TokenHttp执行bug辣!");
177 | e.printStackTrace();
178 | }
179 | }
180 | return null;
181 | }
182 |
183 |
184 | // HashMap写入json文件 不用数组是为了覆盖原key
185 | public static void tokensToFile(HashMap tokenMap, String filePath) {
186 | Type type = new TypeToken>() {
187 | }.getType();
188 | String json = new GsonBuilder().setPrettyPrinting().create().toJson(tokenMap, type);
189 |
190 | try {
191 | FileOutputStream stream = new FileOutputStream(filePath);
192 | stream.write(json.getBytes(StandardCharsets.UTF_8));
193 | stream.close();
194 | } catch(IOException e) {
195 | System.out.println("# TokenToFile执行bug辣!");
196 | throw new RuntimeException(e);
197 | }
198 | }
199 |
200 | // 读取json文件Token Map
201 | public static HashMap tokensFromFile(String filePath, boolean refreshing) {
202 | Type type = new TypeToken>() {
203 | }.getType();
204 | HashMap userMap;
205 | try {
206 | userMap = new Gson().fromJson(new FileReader(filePath), type);
207 | if(userMap==null) { // 空文件判断
208 | return new HashMap<>();
209 | }
210 | // 读取并refresh()
211 | if(refreshing) userMap.forEach((key, token) -> token.refresh());
212 | } catch(FileNotFoundException e) {
213 | throw new RuntimeException(e);
214 | }
215 | return userMap;
216 | }
217 |
218 | public static HashMap tokensFromFile(String filePath) {
219 | return tokensFromFile(filePath, false);
220 | }
221 |
222 |
223 | @Test
224 | public void main() {
225 | TokenBuilder builder = new TokenBuilder();
226 | System.out.println("url:" + builder.getQrcodeUrl());
227 | Token token = builder.getToken();
228 | System.out.println("your id:" + token.getUserId());
229 | }
230 |
231 | @Deprecated
232 | public void autoRefreshToken() {
233 | Scheduler scheduler;
234 | try {
235 | scheduler = new StdSchedulerFactory().getScheduler();
236 | } catch(SchedulerException e) {
237 | throw new RuntimeException(e);
238 | }
239 | JobDetail jobDetail = JobBuilder.newJob(RefreshTokenJob.class).build();
240 | Trigger trigger = TriggerBuilder.newTrigger().startNow().withSchedule(
241 | SimpleScheduleBuilder.repeatSecondlyForever(3))
242 | // .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(13, 36))
243 | .build();
244 | try {
245 | scheduler.scheduleJob(jobDetail, trigger);
246 | scheduler.start();
247 | } catch(SchedulerException e) {
248 | throw new RuntimeException(e);
249 | }
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/MiraiBot.java:
--------------------------------------------------------------------------------
1 | package com.mirai;
2 |
3 | import com.dancecube.token.Token;
4 | import com.dancecube.token.TokenBuilder;
5 | import com.mirai.event.MainHandler;
6 | import com.mirai.task.SchedulerTask;
7 | import net.mamoe.mirai.console.extension.PluginComponentStorage;
8 | import net.mamoe.mirai.console.plugin.jvm.JavaPlugin;
9 | import net.mamoe.mirai.console.plugin.jvm.JavaPluginScheduler;
10 | import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription;
11 | import net.mamoe.mirai.event.Event;
12 | import net.mamoe.mirai.event.EventChannel;
13 | import net.mamoe.mirai.event.GlobalEventChannel;
14 | import net.mamoe.mirai.event.events.MessageEvent;
15 | import net.mamoe.mirai.event.events.NewFriendRequestEvent;
16 | import net.mamoe.mirai.event.events.NudgeEvent;
17 | import org.jetbrains.annotations.NotNull;
18 |
19 | import java.text.SimpleDateFormat;
20 | import java.util.*;
21 | import java.util.logging.Logger;
22 |
23 | import static com.mirai.config.AbstractConfig.configPath;
24 | import static com.mirai.config.AbstractConfig.userTokensMap;
25 |
26 | public final class MiraiBot extends JavaPlugin {
27 | public static final MiraiBot INSTANCE = new MiraiBot();
28 | String path = configPath + "UserTokens.json";
29 |
30 | private MiraiBot() {
31 | super(JvmPluginDescription.loadFromResource("plugin.yml", MiraiBot.class.getClassLoader()));
32 | }
33 |
34 | @Override
35 | public void onLoad(@NotNull PluginComponentStorage $this$onLoad) {
36 | super.onLoad($this$onLoad);
37 | }
38 |
39 | @Override
40 | public void onEnable() {
41 | getLogger().info("Plugin loaded!");
42 | EventChannel channel = GlobalEventChannel.INSTANCE
43 | .parentScope(MiraiBot.INSTANCE)
44 | .context(this.getCoroutineContext());
45 |
46 | // 输出加载Token
47 | onLoadToken();
48 |
49 | // Token刷新器
50 | SchedulerTask.autoRefreshToken();
51 |
52 |
53 | // 监听器
54 | channel.subscribeAlways(MessageEvent.class, MainHandler::eventCenter);
55 | channel.subscribeAlways(NudgeEvent.class, MainHandler::NudgeHandler);
56 | channel.subscribeAlways(NewFriendRequestEvent.class, MainHandler::addFriendHandler);
57 |
58 | }
59 |
60 | @Override
61 | public void onDisable() {
62 | // 保存Tokens
63 | TokenBuilder.tokensToFile(userTokensMap, configPath + "UserTokens.json");
64 | System.out.printf("保存成功!共%d条%n", userTokensMap.size());
65 | }
66 |
67 | @Deprecated
68 | public void refreshTokensTimer() {
69 | long period = 86400 * 500; //半天
70 |
71 | JavaPluginScheduler scheduler = MiraiBot.INSTANCE.getScheduler();
72 | userTokensMap = TokenBuilder.tokensFromFile(path);
73 |
74 | TimerTask task = new TimerTask() {
75 | @Override
76 | public void run() {
77 | Token defaultToken = userTokensMap.get(0L);
78 | if(defaultToken==null) {
79 | userTokensMap.forEach((qq, token) ->
80 | scheduler.async(() -> {
81 | if(token.checkAvailable()) token.refresh();
82 | }));
83 | } else {
84 | userTokensMap.forEach((qq, token) ->
85 | scheduler.async(() -> {
86 | // 默认token不为用户token
87 | if(token.checkAvailable() &
88 | !defaultToken.getAccessToken().equals(token.getAccessToken()))
89 | token.refresh();
90 | }));
91 |
92 | }
93 | TokenBuilder.tokensToFile(userTokensMap, path);
94 | System.out.println(new SimpleDateFormat("MM-dd hh:mm:ss").format(new Date()) + ": 今日已刷新token");
95 | }
96 | };
97 |
98 | new Timer().schedule(task, 0, 86400);
99 | }
100 |
101 | public void onLoadToken() {
102 | StringBuilder sb = new StringBuilder();
103 | // 导入Token
104 | userTokensMap = Objects.requireNonNullElse(
105 | TokenBuilder.tokensFromFile(configPath + "UserTokens.json"),
106 | new HashMap<>());
107 |
108 | for(Map.Entry entry : userTokensMap.entrySet()) {
109 | Long qq = entry.getKey();
110 | Token token = entry.getValue();
111 | sb.append("\nqq: %d , id: %s;".formatted(qq, token.getUserId()));
112 | }
113 | Logger.getGlobal().info(("刷新加载成功!共%d条".formatted(userTokensMap.size()) + sb));
114 | }
115 | }
116 |
117 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/command/AbstractCommand.java:
--------------------------------------------------------------------------------
1 | package com.mirai.command;
2 |
3 | import net.mamoe.mirai.contact.Contact;
4 | import net.mamoe.mirai.event.events.MessageEvent;
5 |
6 | import java.util.HashSet;
7 |
8 | public abstract class AbstractCommand {
9 |
10 | final HashSet scopes = new HashSet<>(); //作用范围
11 | MsgHandleable globalOnCall; //作用效果
12 | MsgHandleable userOnCall;
13 | MsgHandleable groupOnCall;
14 | MsgHandleable adminOnCall;
15 |
16 |
17 |
18 |
19 | public void onCall(Scope scope, MessageEvent event, Contact contact, long qq, String[] args) {
20 |
21 | //不同情况筛选
22 | if(scope==Scope.ADMIN) {
23 | adminOnCall.handle(event, contact, qq, args);
24 | } else if(scope==Scope.GROUP) {
25 | groupOnCall.handle(event, contact, qq, args);
26 | } else if(scope==Scope.USER) {
27 | userOnCall.handle(event, contact, qq, args);
28 | } else {
29 | globalOnCall.handle(event, contact, qq, args);
30 | }
31 | }
32 |
33 | protected void setGlobalOnCall(MsgHandleable onCall) {
34 | globalOnCall = onCall;
35 | userOnCall = onCall;
36 | groupOnCall = onCall;
37 | adminOnCall = onCall;
38 | }
39 |
40 | protected void setUserOnCall(MsgHandleable onCall) {
41 | this.userOnCall = onCall;
42 | }
43 |
44 | protected void setGroupOnCall(MsgHandleable onCall) {
45 | this.groupOnCall = onCall;
46 | }
47 |
48 | protected void setAdminOnCall(MsgHandleable onCall) {
49 | this.adminOnCall = onCall;
50 | }
51 |
52 | public HashSet getScopes() {
53 | return scopes;
54 | }
55 |
56 | protected final void addScope(Scope scope) {
57 | scopes.add(scope);
58 | // clearScopes();
59 | }
60 |
61 | // void clearScopes() {
62 | // if(scopes.contains(Scope.GLOBAL) | ((Scope.values().length - scopes.size()==1) & !scopes.contains(Scope.GLOBAL))) {
63 | // scopes.clear();
64 | // scopes.add(Scope.GLOBAL);
65 | // }
66 | // }
67 | }
68 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/command/ArgsCommand.java:
--------------------------------------------------------------------------------
1 | package com.mirai.command;
2 |
3 | import java.util.regex.Pattern;
4 |
5 | public class ArgsCommand extends AbstractCommand {
6 | public static final Pattern NUMBER = Pattern.compile("\\d+");
7 | public static final Pattern WORD = Pattern.compile("[0-9a-zA-z]+");
8 | public static final Pattern CHAR = Pattern.compile("\\S+");
9 | public static final Pattern ANY = Pattern.compile(".+");
10 |
11 | private String[] prefix;
12 | private Pattern[] form;
13 |
14 | public String[] getPrefix() {
15 | return prefix;
16 | }
17 |
18 | protected void setPrefix(String[] prefix) {
19 | this.prefix = prefix;
20 | }
21 |
22 | protected void setForm(Pattern[] form) {
23 | this.form = form;
24 | }
25 |
26 |
27 | /**
28 | * 检查格式
29 | *
30 | * @param command 需要的指令
31 | * @param args 传递的参数
32 | * @return -1为成功,-2为无参数,否则为匹配错误的索引
33 | */
34 | public static int checkError(ArgsCommand command, String[] args) {
35 | if(args == null) return -2;
36 |
37 | if(args.length command.setUserOnCall(onCall);
28 | case GROUP -> command.setGroupOnCall(onCall);
29 | case ADMIN -> command.setAdminOnCall(onCall);
30 | }
31 | }
32 | return this;
33 | }
34 |
35 | public ArgsCommand build() {
36 | return command;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/command/DeclaredCommand.java:
--------------------------------------------------------------------------------
1 | package com.mirai.command;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | /**
9 | * 用于注册指令到监听器,
10 | * 仅注解了 @DeclaredCommand 的 Command 对象才会被放入 Handler 以监听事件
11 | *
12 | * @author Lin
13 | */
14 | @Retention(RetentionPolicy.RUNTIME)
15 | @Target(ElementType.FIELD)
16 | public @interface DeclaredCommand {
17 | //指令名称或描述,不必要填写
18 | String value();
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/command/MsgHandleable.java:
--------------------------------------------------------------------------------
1 | package com.mirai.command;
2 |
3 |
4 | import net.mamoe.mirai.contact.Contact;
5 | import net.mamoe.mirai.event.events.MessageEvent;
6 | import org.jetbrains.annotations.Nullable;
7 |
8 | /**
9 | * 消息处理接口
10 | */
11 | @FunctionalInterface
12 | public interface MsgHandleable {
13 | /**
14 | * 类似于 Consumer 但是定义好了方法参数,通过Lambda表达式传值
15 | *
16 | * @param event 消息事件
17 | * @param contact 消息发送者
18 | * @param qq 消息发送者的QQ号
19 | * @param args 消息参数,传入{@code RegexCommand}对象或者没有参数时为null
20 | */
21 | void handle(MessageEvent event, Contact contact, long qq, @Nullable String[] args);
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/command/RegexCommand.java:
--------------------------------------------------------------------------------
1 | package com.mirai.command;
2 |
3 | import java.util.regex.Pattern;
4 |
5 | // 原始指令
6 | public class RegexCommand extends AbstractCommand {
7 | private Pattern regex; //正则
8 |
9 | protected RegexCommand() {
10 | }
11 |
12 | protected void setRegex(Pattern regex) {
13 | this.regex = regex;
14 | }
15 |
16 | public Pattern getRegex() {
17 | return regex;
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/command/RegexCommandBuilder.java:
--------------------------------------------------------------------------------
1 | package com.mirai.command;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 |
5 | import java.util.regex.Pattern;
6 |
7 | // 构造器
8 | public class RegexCommandBuilder {
9 | private final RegexCommand command = new RegexCommand();
10 |
11 | public RegexCommandBuilder regex(@NotNull String regex) {
12 | return regex(regex, true);
13 | }
14 |
15 | public RegexCommandBuilder regex(String regex, boolean lineOnly) {
16 | if(regex.isBlank()) throw new RuntimeException("regex不能为空值");
17 | command.setRegex(Pattern.compile(lineOnly ? "^" + regex + "$" : regex));
18 | return this;
19 | }
20 |
21 | // TODO lineOnly -> ^(...|...)$
22 | public RegexCommandBuilder multiStrings(String... strings) {
23 | if(strings.length<1) throw new RuntimeException("regex不能为空值");
24 | StringBuilder builder = new StringBuilder();
25 | for(String string : strings) {
26 | builder.append('|').append('^').append(string).append('$');
27 | }
28 | builder.deleteCharAt(0);
29 | command.setRegex(Pattern.compile(builder.toString()));
30 | return this;
31 | }
32 |
33 | public RegexCommandBuilder onCall(Scope scope, @NotNull MsgHandleable onCall) {
34 | command.addScope(scope);
35 | if(scope==Scope.GLOBAL) {
36 | command.setGlobalOnCall(onCall);
37 | } else {
38 | switch(scope) {
39 | case USER -> command.setUserOnCall(onCall);
40 | case GROUP -> command.setGroupOnCall(onCall);
41 | case ADMIN -> command.setAdminOnCall(onCall);
42 | }
43 | }
44 | return this;
45 | }
46 |
47 | public RegexCommand build() {
48 | return command;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/command/ReplyCommand.java:
--------------------------------------------------------------------------------
1 | package com.mirai.command;
2 |
3 | public class ReplyCommand extends AbstractCommand {
4 | private String[] recvMessages;
5 | private String[] replyMessages;
6 |
7 | public void setReplyMessages(String[] replyMessages) {
8 | this.replyMessages = replyMessages;
9 | }
10 |
11 | public void setRecvMessages(String[] recvMessages) {
12 | this.recvMessages = recvMessages;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/command/ReplyCommandBuilder.java:
--------------------------------------------------------------------------------
1 | package com.mirai.command;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 |
5 | public class ReplyCommandBuilder {
6 | private final ReplyCommand command = new ReplyCommand();
7 |
8 | public ReplyCommandBuilder replyMessages(String... strings) {
9 | command.setRecvMessages(strings);
10 | return this;
11 | }
12 |
13 | public ReplyCommandBuilder recvMessages(String... strings) {
14 | command.setRecvMessages(strings);
15 | return this;
16 | }
17 |
18 | public ReplyCommandBuilder onCall(Scope scope, @NotNull MsgHandleable onCall) {
19 | command.addScope(scope);
20 | if(scope==Scope.GLOBAL) {
21 | command.setGlobalOnCall(onCall);
22 | } else {
23 | switch(scope) {
24 | case USER -> command.setUserOnCall(onCall);
25 | case GROUP -> command.setGroupOnCall(onCall);
26 | case ADMIN -> command.setAdminOnCall(onCall);
27 | }
28 | }
29 | return this;
30 | }
31 |
32 | public ReplyCommand build() {
33 | return command;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/command/Scope.java:
--------------------------------------------------------------------------------
1 | package com.mirai.command;
2 |
3 | public enum Scope {
4 | GLOBAL, //全局
5 | USER, //私聊
6 | GROUP, //群聊
7 | ADMIN //机器人管理员
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/config/AbstractConfig.java:
--------------------------------------------------------------------------------
1 | package com.mirai.config;
2 |
3 | import com.dancecube.token.Token;
4 | import org.yaml.snakeyaml.Yaml;
5 |
6 | import java.io.File;
7 | import java.io.IOException;
8 | import java.nio.file.Files;
9 | import java.util.HashMap;
10 | import java.util.HashSet;
11 | import java.util.Map;
12 |
13 | public abstract class AbstractConfig {
14 | public static HashMap userTokensMap;
15 | public static HashSet logStatus = new HashSet<>();
16 | public static String linuxRootPath;
17 | public static String windowsRootPath;
18 | private static final boolean windowsMark;
19 | public static String configPath;
20 |
21 | // api key
22 | public static String gaodeApiKey = "";
23 | public static String tencentSecretId = "";
24 | public static String tencentSecretKey = "";
25 |
26 | static {
27 |
28 | windowsMark = new File("./WINDOWS_MARK").exists();
29 | try {
30 | linuxRootPath = new File("..").getCanonicalPath();
31 | windowsRootPath = new File(".").getCanonicalPath();
32 |
33 | //在项目下创建 “WINDOWS_MARK” 文件,存在即使用Windows路径的配置,而Linux则不需要
34 | if(itIsAReeeeaaaalWindowsMark()) {
35 | configPath = windowsRootPath + "/DcConfig/";
36 | } else {
37 | configPath = linuxRootPath + "/DcConfig/";
38 | }
39 | new File(configPath).mkdirs();
40 | } catch(IOException e) {
41 | e.printStackTrace();
42 | }
43 |
44 |
45 | // Authorization错误时查看控制台ip白名单
46 | Map> map;
47 | try {
48 | File apiKeyYml = new File(configPath + "ApiKeys.yml");
49 | if(!apiKeyYml.exists()) {
50 | apiKeyYml.getParentFile().mkdirs();
51 | apiKeyYml.createNewFile();
52 | }
53 |
54 | map = new Yaml().load(Files.readString(apiKeyYml.toPath()));
55 | } catch(IOException e) {
56 | throw new RuntimeException(e);
57 | }
58 | try {
59 | Map tencentScannerKeys = map.get("tencentScannerKeys");
60 | Map gaodeMapKeys = map.get("gaodeMapKeys");
61 |
62 | gaodeApiKey = gaodeMapKeys.get("apiKey");
63 | tencentSecretId = tencentScannerKeys.get("secretId");
64 | tencentSecretKey = tencentScannerKeys.get("secretKey");
65 | } catch(NullPointerException e) {
66 | System.out.println("# ApiKey配置不完整!");
67 | e.printStackTrace();
68 | }
69 | }
70 |
71 | /**
72 | * 让我看看是不是Windows!
73 | *
74 | * @return 这是个Windows系统
75 | */
76 | public static boolean itIsAReeeeaaaalWindowsMark() {
77 | return windowsMark;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/config/UserConfigUtils.java:
--------------------------------------------------------------------------------
1 | package com.mirai.config;
2 |
3 | import com.google.gson.Gson;
4 | import com.google.gson.GsonBuilder;
5 | import com.google.gson.reflect.TypeToken;
6 |
7 | import java.io.FileNotFoundException;
8 | import java.io.FileOutputStream;
9 | import java.io.FileReader;
10 | import java.io.IOException;
11 | import java.lang.reflect.Type;
12 | import java.nio.charset.StandardCharsets;
13 | import java.util.HashMap;
14 | import java.util.HashSet;
15 |
16 | public class UserConfigUtils extends AbstractConfig {
17 | public static HashMap> configsFromFile(String filePath) {
18 | Type type = new TypeToken>>() {
19 | }.getType();
20 | HashMap> map = null;
21 | try {
22 | map = new Gson().fromJson(new FileReader(filePath), type);
23 | } catch(FileNotFoundException e) {
24 | e.printStackTrace();
25 | }
26 | return map;
27 | }
28 |
29 | public static void configsToFile(HashMap> map, String filePath) {
30 | String json = new GsonBuilder().setPrettyPrinting().create().toJson(map);
31 | try {
32 | FileOutputStream stream = new FileOutputStream(filePath);
33 | stream.write(json.getBytes(StandardCharsets.UTF_8));
34 | stream.close();
35 | } catch(IOException e) {
36 | e.printStackTrace();
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/event/MainHandler.java:
--------------------------------------------------------------------------------
1 | package com.mirai.event;
2 |
3 | import com.dancecube.token.Token;
4 | import com.dancecube.token.TokenBuilder;
5 | import net.mamoe.mirai.Bot;
6 | import net.mamoe.mirai.contact.Contact;
7 | import net.mamoe.mirai.contact.Friend;
8 | import net.mamoe.mirai.contact.friendgroup.FriendGroup;
9 | import net.mamoe.mirai.event.EventHandler;
10 | import net.mamoe.mirai.event.events.MessageEvent;
11 | import net.mamoe.mirai.event.events.NewFriendRequestEvent;
12 | import net.mamoe.mirai.event.events.NudgeEvent;
13 | import net.mamoe.mirai.message.data.At;
14 | import net.mamoe.mirai.message.data.MessageChain;
15 | import net.mamoe.mirai.message.data.PlainText;
16 |
17 | import static com.mirai.config.AbstractConfig.configPath;
18 | import static com.mirai.config.AbstractConfig.userTokensMap;
19 |
20 | // 不过滤通道
21 | public class MainHandler {
22 |
23 | @EventHandler
24 | public static void eventCenter(MessageEvent event) {
25 | MessageChain messageChain = event.getMessage();
26 | if(messageChain.size() - 1==messageChain.stream()
27 | .filter(msg -> msg instanceof At | msg instanceof PlainText)
28 | .toList().size()) {
29 | PlainTextHandler.accept(event);
30 | } else return;
31 |
32 | String message = messageChain.contentToString();
33 | long qq = event.getSender().getId(); // qq发送者id 而非群聊id
34 | Contact contact = event.getSubject();
35 |
36 | // 文本消息检测
37 | switch(message) {
38 | case "#save" -> saveTokens(contact);
39 | case "#load" -> loadTokens(contact);
40 | case "#logout" -> logoutToken(contact);
41 | }
42 | }
43 |
44 | @EventHandler
45 | public static void NudgeHandler(NudgeEvent event) {
46 | if(event.getTarget() instanceof Bot) {
47 | event.getFrom().nudge().sendTo(event.getSubject());
48 | }
49 | }
50 |
51 | @EventHandler
52 | public static void addFriendHandler(NewFriendRequestEvent event) {
53 | event.accept();
54 | Friend friend = event.getBot().getFriend(event.getFromId());
55 | if(friend != null) {
56 | friend.sendMessage("🥰呐~ 现在我们是好朋友啦!\n请到主页查看功能哦!");
57 | FriendGroup friendGroup = event.getBot().getFriendGroups().get(0);
58 | if(friendGroup != null) {
59 | friendGroup.moveIn(friend);
60 | }
61 | }
62 | }
63 |
64 |
65 | /**
66 | * 保存Token到文件JSON
67 | *
68 | * @param contact 触发对象
69 | */
70 | public static void saveTokens(Contact contact) {
71 | TokenBuilder.tokensToFile(userTokensMap, configPath + "UserTokens.json");
72 | contact.sendMessage("保存成功!共%d条".formatted(userTokensMap.size()));
73 | }
74 |
75 | /**
76 | * 从文件JSON中加载Token
77 | *
78 | * @param contact 触发对象
79 | */
80 | public static void loadTokens(Contact contact) {
81 | String path = configPath + "UserTokens.json";
82 | userTokensMap = TokenBuilder.tokensFromFile(path, false);
83 | contact.sendMessage("不刷新加载成功!共%d条".formatted(userTokensMap.size()));
84 | }
85 |
86 | /**
87 | * 注销Token
88 | *
89 | * @param contact 触发对象
90 | */
91 | public static void logoutToken(Contact contact) {
92 | long qq = contact.getId();
93 | Token token = userTokensMap.get(qq);
94 | if(token == null) {
95 | contact.sendMessage("当前账号未登录到舞小铃!");
96 | return;
97 | }
98 | userTokensMap.remove(qq);
99 | contact.sendMessage("id:%d 注销成功!".formatted(token.getUserId()));
100 | }
101 |
102 | }
--------------------------------------------------------------------------------
/src/main/java/com/mirai/event/PlainTextHandler.java:
--------------------------------------------------------------------------------
1 | package com.mirai.event;
2 |
3 | import com.mirai.command.*;
4 | import net.mamoe.mirai.contact.Contact;
5 | import net.mamoe.mirai.contact.Group;
6 | import net.mamoe.mirai.contact.User;
7 | import net.mamoe.mirai.event.events.MessageEvent;
8 | import net.mamoe.mirai.message.data.At;
9 | import net.mamoe.mirai.message.data.MessageChain;
10 |
11 | import java.util.ArrayList;
12 | import java.util.Arrays;
13 | import java.util.Calendar;
14 | import java.util.HashSet;
15 |
16 | public class PlainTextHandler {
17 |
18 | public static HashSet adminsSet = new HashSet<>();
19 |
20 | static {
21 | // 初始化AllCommands所有指令
22 | AllCommands.init();
23 | adminsSet.add(2862125721L);
24 | }
25 |
26 | public static HashSet regexCommands = AllCommands.regexCommands; //所有正则指令
27 | public static HashSet argsCommands = AllCommands.argsCommands; //所有参数指令
28 |
29 | private static final int MAX_LENGTH = 0x7fff; //单次指令字符最大长度(用于过滤)
30 |
31 |
32 | /**
33 | * 事件处理
34 | *
35 | * @param messageEvent 消息事件
36 | */
37 | public static void accept(MessageEvent messageEvent) {
38 | MessageChain messageChain = messageEvent.getMessage();
39 |
40 | String message;
41 |
42 | if(messageChain.stream().anyMatch(m -> m instanceof At)) { //At 转 QQ
43 | StringBuilder builder = new StringBuilder();
44 | messageChain.forEach(msg -> builder.append(" ").append(msg instanceof At ? (((At) msg).getTarget()) : msg.contentToString()));
45 | message = builder.toString();
46 | } else {
47 | message = messageChain.contentToString();
48 | }
49 | if(message.length()>MAX_LENGTH) return;
50 |
51 |
52 | // 执行正则指令
53 | for(RegexCommand regexCommand : regexCommands) {// 匹配作用域
54 | boolean find = regexCommand.getRegex().matcher(message).find();
55 | if(find) {
56 | runCommand(messageEvent, regexCommand);
57 | }
58 | }
59 |
60 | ArrayList prefixAndArgs = new ArrayList<>(Arrays.asList(message.strip().split("\\s+")));
61 | String msgPre = prefixAndArgs.remove(0); //前缀
62 | String[] args = prefixAndArgs.isEmpty() ? null : prefixAndArgs.toArray(new String[0]); //参数 奇奇怪怪的特性,这不是空数组!
63 |
64 |
65 | //执行参数指令
66 | for(ArgsCommand command : argsCommands) {
67 | String[] commandPrefixes = command.getPrefix();
68 | if(Arrays.asList(commandPrefixes).contains(msgPre)) {
69 | if(ArgsCommand.checkError(command, args) < 0) { //args可能不存在,需要判空
70 | runCommand(messageEvent, command, args);
71 | }
72 | }
73 | }
74 |
75 |
76 | }
77 |
78 | private static final MsgHandleable MUTE = (event, contact, qq, args) -> contact.sendMessage("小铃困啦,白天再来玩吧,先晚安安啦~");
79 |
80 | /**
81 | * 宵禁
82 | *
83 | * @return 是否在宵禁
84 | */
85 | public static boolean isMutedNow() {
86 | // 获取当前时间
87 | int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
88 | // 如果当前时间是 3 点到 4 点,则将 `autoMuted` 设置为 true
89 | return hour>=3 && hour<=4;
90 | }
91 |
92 | //含参指令
93 | private static void runCommand(MessageEvent messageEvent, AbstractCommand command, String[] args) {
94 |
95 | HashSet scopes = command.getScopes(); //作用域
96 | long qq = messageEvent.getSender().getId(); // qq不为contact.getId()
97 | Contact contact = messageEvent.getSubject(); //发送对象
98 |
99 | if(isMutedNow()) {
100 | MUTE.handle(messageEvent, contact, qq, args);
101 | if(!adminsSet.contains(qq)) return;
102 | }
103 |
104 | if(scopes.contains(Scope.GLOBAL))
105 | command.onCall(Scope.GLOBAL, messageEvent, contact, qq, args);
106 | else if((scopes.contains(Scope.USER) & contact instanceof User))
107 | command.onCall(Scope.USER, messageEvent, contact, qq, args);
108 | else if((scopes.contains(Scope.GROUP) & contact instanceof Group))
109 | command.onCall(Scope.GROUP, messageEvent, contact, qq, args);
110 | else if(scopes.contains(Scope.ADMIN) & adminsSet.contains(qq))
111 | command.onCall(Scope.ADMIN, messageEvent, contact, qq, args);
112 | }
113 |
114 | // 无参指令(其实就是给上面的runCommand传了个args=null)
115 | private static void runCommand(MessageEvent messageEvent, AbstractCommand command) {
116 | runCommand(messageEvent, command, null);
117 | }
118 | }
119 |
120 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/task/RefreshTokenJob.java:
--------------------------------------------------------------------------------
1 | package com.mirai.task;
2 |
3 | import com.dancecube.token.Token;
4 | import com.dancecube.token.TokenBuilder;
5 | import com.mirai.config.AbstractConfig;
6 | import org.quartz.Job;
7 | import org.quartz.JobExecutionContext;
8 | import org.quartz.JobExecutionException;
9 |
10 | import java.text.SimpleDateFormat;
11 | import java.util.Date;
12 | import java.util.HashMap;
13 | import java.util.concurrent.atomic.AtomicInteger;
14 |
15 | import static com.mirai.config.AbstractConfig.configPath;
16 |
17 | /*
18 | 为什么设计成JobDetail + Job,不直接使用Job?
19 |
20 | JobDetail 定义的是任务数据,而真正的执行逻辑是在Job中。
21 | 这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。
22 | 而JobDetail & Job 方式,Scheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以 规避并发访问 的问题
23 | */
24 |
25 | /**
26 | * 刷新本地Token任务
27 | *
28 | * @author Lin
29 | */
30 | public class RefreshTokenJob implements Job {
31 | private static final String dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
32 | private static final HashMap userTokensMap = AbstractConfig.userTokensMap;
33 |
34 | public void execute(JobExecutionContext context) throws JobExecutionException {
35 | AtomicInteger validNum = new AtomicInteger();
36 | userTokensMap.forEach(
37 | (qq, token) -> {
38 | if(token.refresh()) validNum.getAndIncrement();
39 | }
40 | );
41 | TokenBuilder.tokensToFile(userTokensMap, configPath + "UserTokens.json");
42 | System.out.printf("#%s 读取共%d个token,有效token共%d个%n", dateFormat, userTokensMap.size(), validNum.get());
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/java/com/mirai/task/SchedulerTask.java:
--------------------------------------------------------------------------------
1 | package com.mirai.task;
2 |
3 | import org.quartz.*;
4 | import org.quartz.impl.StdSchedulerFactory;
5 |
6 | public class SchedulerTask {
7 |
8 | public static void autoRefreshToken() {
9 | Scheduler scheduler;
10 | try {
11 | scheduler = new StdSchedulerFactory().getScheduler();
12 | } catch(SchedulerException e) {
13 | throw new RuntimeException(e);
14 | }
15 | JobDetail jobDetail = JobBuilder.newJob(RefreshTokenJob.class).build();
16 | Trigger trigger = TriggerBuilder.newTrigger().startNow()
17 | // .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(60))
18 | .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 10))
19 | .build();
20 | try {
21 | scheduler.scheduleJob(jobDetail, trigger);
22 | scheduler.start();
23 | } catch(SchedulerException e) {
24 | throw new RuntimeException(e);
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/com/tools/DanceCubeRequestCrypto.java:
--------------------------------------------------------------------------------
1 | package com.tools;
2 |
3 | import com.google.gson.JsonObject;
4 |
5 | import javax.crypto.BadPaddingException;
6 | import javax.crypto.Cipher;
7 | import javax.crypto.IllegalBlockSizeException;
8 | import javax.crypto.NoSuchPaddingException;
9 | import javax.crypto.spec.IvParameterSpec;
10 | import javax.crypto.spec.SecretKeySpec;
11 | import java.nio.charset.StandardCharsets;
12 | import java.security.InvalidAlgorithmParameterException;
13 | import java.security.InvalidKeyException;
14 | import java.security.NoSuchAlgorithmException;
15 | import java.util.Base64;
16 | import java.util.Map;
17 |
18 | public class DanceCubeRequestCrypto {
19 | public static final String KEY = "3339363237333738";
20 | public static final String IV = "3339363237333738";
21 |
22 |
23 | public static String dataBuild(String path,
24 | Map params,
25 | String method,
26 | JsonObject data,
27 | String contentType) {
28 | String paramsStr = convertMapToString(params);
29 |
30 | String result = "{\"path\":\"%s\",\"param\":\"%s\",\"data\":%s,\"method\":\"%s\",\"contentType\":\"%s\"}"
31 | .formatted(path, paramsStr, method, data.toString(), contentType);
32 | return result;
33 |
34 | }
35 |
36 | public static String decryptWithDesCbc(String cipherText, byte[] key, byte[] iv) {
37 | Cipher cipher = null;
38 | String result = null;
39 | try {
40 | cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
41 | cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "DES"), new IvParameterSpec(iv));
42 |
43 | byte[] ciphertext = Base64.getDecoder().decode(cipherText);
44 | byte[] plaintext = cipher.doFinal(ciphertext);
45 | result = new String(plaintext, StandardCharsets.UTF_8);
46 | } catch(NoSuchAlgorithmException | NoSuchPaddingException e) {
47 | throw new RuntimeException("No such algorithm or padding", e);
48 | } catch(InvalidKeyException | InvalidAlgorithmParameterException e) {
49 | throw new RuntimeException("Invalid key or iv", e);
50 | } catch(IllegalBlockSizeException | BadPaddingException e) {
51 | throw new RuntimeException("Illegal block size or bad padding in doFinal()", e);
52 | }
53 | return result;
54 | }
55 |
56 | public static String cryptoWithDesCbc(String plainText, byte[] key, byte[] iv) {
57 | Cipher cipher = null;
58 | String result = null;
59 | try {
60 | cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
61 | cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "DES"), new IvParameterSpec(iv));
62 |
63 | byte[] ciphertext = Base64.getDecoder().decode(plainText);
64 | byte[] plaintext = cipher.doFinal(ciphertext);
65 | result = new String(plaintext, StandardCharsets.UTF_8);
66 | } catch(NoSuchAlgorithmException | NoSuchPaddingException e) {
67 | throw new RuntimeException("No such algorithm or padding", e);
68 | } catch(InvalidKeyException | InvalidAlgorithmParameterException e) {
69 | throw new RuntimeException("Invalid key or iv", e);
70 | } catch(IllegalBlockSizeException | BadPaddingException e) {
71 | throw new RuntimeException("Illegal block size or bad padding in doFinal()", e);
72 | }
73 | return result;
74 | }
75 |
76 | private static byte[] hexToBytes(String hex) {
77 | int len = hex.length();
78 | byte[] bytes = new byte[len / 2];
79 | for(int i = 0; i < len; i += 2) {
80 | bytes[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
81 | + Character.digit(hex.charAt(i + 1), 16));
82 | }
83 | return bytes;
84 | }
85 |
86 | public static String convertMapToString(Map map) {
87 | StringBuilder result = new StringBuilder();
88 | for(Map.Entry entry : map.entrySet()) {
89 | if(!result.isEmpty()) {
90 | result.append("&");
91 | }
92 | result.append(entry.getKey()).append("=").append(entry.getValue());
93 | }
94 | return result.toString();
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/main/java/com/tools/HttpUtil.java:
--------------------------------------------------------------------------------
1 | package com.tools;
2 |
3 | import com.google.zxing.*;
4 | import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
5 | import com.google.zxing.common.HybridBinarizer;
6 | import com.mirai.config.AbstractConfig;
7 | import com.tencentcloudapi.common.Credential;
8 | import com.tencentcloudapi.common.exception.TencentCloudSDKException;
9 | import com.tencentcloudapi.common.profile.ClientProfile;
10 | import com.tencentcloudapi.common.profile.HttpProfile;
11 | import com.tencentcloudapi.ocr.v20181119.OcrClient;
12 | import com.tencentcloudapi.ocr.v20181119.models.QrcodeOCRRequest;
13 | import com.tencentcloudapi.ocr.v20181119.models.QrcodeOCRResponse;
14 | import net.mamoe.mirai.contact.Contact;
15 | import net.mamoe.mirai.message.data.Image;
16 | import net.mamoe.mirai.utils.ExternalResource;
17 | import okhttp3.*;
18 | import org.jetbrains.annotations.NotNull;
19 | import org.jetbrains.annotations.Nullable;
20 |
21 | import javax.imageio.ImageIO;
22 | import java.awt.image.BufferedImage;
23 | import java.io.IOException;
24 | import java.io.InputStream;
25 | import java.net.URL;
26 | import java.net.URLConnection;
27 | import java.util.HashMap;
28 | import java.util.Map;
29 |
30 |
31 | public class HttpUtil {
32 |
33 | public static Image getImageFromURL(String strUrl, Contact contact) {
34 | Image image = null;
35 | try {
36 | URL url = new URL(strUrl);
37 | URLConnection uc = url.openConnection();
38 | InputStream in = uc.getInputStream();
39 | byte[] bytes = in.readAllBytes();
40 | in.close();
41 | ExternalResource ex = ExternalResource.create(bytes);
42 | image = ExternalResource.uploadAsImage(ex, contact);
43 | ex.close();
44 | } catch(IOException e) {
45 | e.printStackTrace();
46 | System.out.println("# getImageFromURL出bug啦!");
47 | }
48 | return image;
49 | }
50 |
51 | public static Image getImageFromStream(InputStream inputStream, Contact contact) {
52 | try {
53 | return getImageFromBytes(inputStream.readAllBytes(), contact);
54 | } catch(IOException e) {
55 | throw new RuntimeException(e);
56 | }
57 | }
58 |
59 | public static Image getImageFromBytes(byte[] bytes, Contact contact) {
60 | Image image = null;
61 | try {
62 | ExternalResource ex = ExternalResource.create(bytes);
63 | image = ExternalResource.uploadAsImage(ex, contact);
64 | ex.close();
65 | } catch(IOException e) {
66 | e.printStackTrace();
67 | System.out.println("# getImageFromIS出bug啦!");
68 | }
69 | return image;
70 | }
71 |
72 |
73 | /**
74 | * @param url 使用URL格式,即“ http:// ”和“ file:/// ”
75 | */
76 | public static String qrDecodeZXing(String url) {
77 | BufferedImage bufferedImage = null;
78 | try {
79 | bufferedImage = ImageIO.read(new URL(url));
80 | } catch(IOException e) {
81 | e.printStackTrace();
82 | }
83 | LuminanceSource source = null;
84 | if(bufferedImage!=null) source = new BufferedImageLuminanceSource(bufferedImage);
85 | Binarizer binarizer = new HybridBinarizer(source);
86 | BinaryBitmap bitmap = new BinaryBitmap(binarizer);
87 | HashMap decodeHints = new HashMap<>();
88 | decodeHints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
89 | Result result = null;
90 | try {
91 | result = new MultiFormatReader().decode(bitmap, decodeHints);
92 | } catch(NotFoundException e) {
93 | e.printStackTrace();
94 | }
95 | return result==null ? "" : result.getText();
96 | }
97 |
98 | public static String qrDecodeTencent(String imgUrl) {
99 | String url = "";
100 | try {
101 | Credential cred = new Credential(AbstractConfig.tencentSecretId, AbstractConfig.tencentSecretKey);
102 | // 实例化一个http选项,可选的,没有特殊需求可以跳过
103 | HttpProfile httpProfile = new HttpProfile();
104 | httpProfile.setEndpoint("ocr.tencentcloudapi.com");
105 | // 实例化一个client选项,可选的,没有特殊需求可以跳过
106 | ClientProfile clientProfile = new ClientProfile();
107 | clientProfile.setHttpProfile(httpProfile);
108 | // 实例化要请求产品的client对象,clientProfile是可选的
109 | OcrClient client = new OcrClient(cred, "ap-shanghai", clientProfile);
110 | // 实例化一个请求对象,每个接口都会对应一个request对象
111 | QrcodeOCRRequest req = new QrcodeOCRRequest();
112 | req.setImageUrl(imgUrl);
113 | // 返回的resp是一个QrcodeOCRResponse的实例,与请求对象对应
114 | QrcodeOCRResponse resp = client.QrcodeOCR(req);
115 | // 输出json格式的字符串回包
116 | url = resp.getCodeResults()[0].getUrl();
117 | } catch(TencentCloudSDKException e) {
118 | e.printStackTrace();
119 | }
120 | return url;
121 | }
122 |
123 | public static String getLocationInfo(String region) {
124 | Response response = httpApi("https://restapi.amap.com/v3/geocode/geo?address=" + region.strip() + "&output=json&key=" + AbstractConfig.gaodeApiKey);
125 | String result = "";
126 | try {
127 | if(response!=null && response.body()!=null) {
128 | result = response.body().string();
129 | response.close();
130 | }
131 | } catch(IOException e) {
132 | e.printStackTrace();
133 | }
134 | return result;
135 | }
136 |
137 | @Nullable // 用于获取HTTP API资源
138 | public static Response httpApi(String url) {
139 | Request request = new Request.Builder().url(url).get().build();
140 | OkHttpClient client = new OkHttpClient();
141 |
142 | try {
143 | return client.newCall(request).execute();
144 | } catch(IOException e) {
145 | e.printStackTrace();
146 | }
147 | return null;
148 | }
149 |
150 | @Nullable // GET
151 | public static Response httpApi(String url, @NotNull Map headersMap) {
152 | OkHttpClient client = new OkHttpClient();
153 | Request request = new Request.Builder().url(url).get().headers(Headers.of(headersMap)).build();
154 |
155 | try {
156 | return client.newCall(request).execute();
157 | } catch(IOException e) {
158 | e.printStackTrace();
159 | }
160 | return null;
161 | }
162 |
163 | /**
164 | * @param bodyMap MediaType 默认为 application/x-www-form-urlencoded, null时则为POST请求
165 | */
166 | @Nullable // POST
167 | public static Response httpApi(String url, @NotNull Map headersMap, @Nullable Map bodyMap) {
168 |
169 | if(bodyMap==null) bodyMap = Map.of("", "");
170 | String body = bodyOf(bodyMap);
171 | OkHttpClient client = new OkHttpClient();
172 | MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");
173 | RequestBody requestBody = RequestBody.create(body, mediaType);
174 | Request request = new Request.Builder().url(url).headers(Headers.of(headersMap)).post(requestBody).build();
175 |
176 | try {
177 | return client.newCall(request).execute();
178 | } catch(IOException e) {
179 | e.printStackTrace();
180 | }
181 | return null;
182 | }
183 |
184 | public static Response httpApiPut(String url, @NotNull Map headersMap, Map bodyMap) {
185 | String body = bodyOf(bodyMap);
186 |
187 | OkHttpClient client = new OkHttpClient();
188 | MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");
189 | RequestBody requestBody = RequestBody.create(body, mediaType);
190 |
191 | Request request = new Request.Builder().url(url).headers(Headers.of(headersMap)).put(requestBody).build();
192 | try {
193 | return client.newCall(request).execute();
194 | } catch(IOException e) {
195 | throw new RuntimeException(e);
196 | }
197 | }
198 |
199 |
200 | public static Call httpApiCall(String url, @NotNull Map headersMap, Map bodyMap) {
201 |
202 | String body = bodyOf(bodyMap);
203 |
204 | OkHttpClient client = new OkHttpClient();
205 | MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");
206 | RequestBody requestBody = RequestBody.create(body, mediaType);
207 |
208 | Request request = new Request.Builder().url(url).headers(Headers.of(headersMap)).post(requestBody).build();
209 | return client.newCall(request);
210 | }
211 |
212 | public static Call httpApiCall(String url, @NotNull Map headersMap) {
213 | OkHttpClient client = new OkHttpClient();
214 | Request request = new Request.Builder().url(url).headers(Headers.of(headersMap)).get().build();
215 | return client.newCall(request);
216 | }
217 |
218 | @NotNull
219 | private static String bodyOf(Map bodyMap) {
220 | StringBuilder bodySb = new StringBuilder();
221 | bodyMap.forEach((k, v) -> bodySb.append('&').append(k).append('=').append(v));
222 | bodySb.deleteCharAt(0);
223 | return bodySb.toString();
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/src/main/java/com/tools/JsonUtil.java:
--------------------------------------------------------------------------------
1 | package com.tools;
2 |
3 | import com.google.gson.JsonElement;
4 | import com.google.gson.JsonObject;
5 | import org.jetbrains.annotations.NotNull;
6 |
7 | import java.util.Map;
8 |
9 | public class JsonUtil {
10 |
11 | @NotNull
12 | public static JsonObject mergeJson(JsonObject... jsonObjects) {
13 | JsonObject mergedObject = new JsonObject();
14 | for(JsonObject json : jsonObjects) {
15 | for(Map.Entry entry : json.entrySet()) {
16 | mergedObject.add(entry.getKey(), entry.getValue());
17 | }
18 | }
19 | return mergedObject;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/com/tools/image/ImageDrawer.java:
--------------------------------------------------------------------------------
1 | package com.tools.image;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 |
5 | import javax.imageio.IIOImage;
6 | import javax.imageio.ImageIO;
7 | import javax.imageio.ImageWriteParam;
8 | import javax.imageio.ImageWriter;
9 | import java.awt.*;
10 | import java.awt.image.BufferedImage;
11 | import java.io.*;
12 | import java.net.URL;
13 | import java.util.Iterator;
14 |
15 | public class ImageDrawer {
16 | private final BufferedImage originImage;
17 | private final Graphics2D graphics;
18 |
19 | public ImageDrawer(BufferedImage image) {
20 | originImage = image;
21 | graphics = originImage.createGraphics();
22 | graphics.setColor(Color.BLACK);
23 | }
24 |
25 | public ImageDrawer(File file) {
26 | try {
27 | originImage = ImageIO.read(new FileInputStream(file));
28 | graphics = originImage.createGraphics();
29 | graphics.setColor(Color.BLACK);
30 | } catch(IOException e) {
31 | throw new RuntimeException(e);
32 | }
33 | }
34 |
35 | public ImageDrawer(String url) {
36 | try {
37 | originImage = ImageIO.read(new URL(url));
38 | } catch(IOException e) {
39 | throw new RuntimeException(e);
40 | }
41 | graphics = originImage.createGraphics();
42 | graphics.setColor(Color.BLACK);
43 | }
44 |
45 | public ImageDrawer color(Color color) {
46 | graphics.setColor(color);
47 | return this;
48 | }
49 |
50 | public ImageDrawer font(Font font) {
51 | graphics.setFont(font);
52 | return this;
53 | }
54 |
55 | public ImageDrawer paint(Paint paint) {
56 | graphics.setPaint(paint);
57 | return this;
58 | }
59 |
60 | public ImageDrawer clip(Shape clip) {
61 | graphics.setClip(clip);
62 | return this;
63 | }
64 |
65 |
66 | public ImageDrawer clip(int x, int y, int width, int height) {
67 | graphics.setClip(x, y, width, height);
68 | return this;
69 | }
70 |
71 | private BufferedImage makeBlur(BufferedImage srcImage, int radius) {
72 |
73 | if(radius<1) {
74 | return srcImage;
75 | }
76 |
77 | int w = srcImage.getWidth();
78 | int h = srcImage.getHeight();
79 |
80 | int[] pix = new int[w * h];
81 | srcImage.getRGB(0, 0, w, h, pix, 0, w);
82 |
83 | int wm = w - 1;
84 | int hm = h - 1;
85 | int wh = w * h;
86 | int div = radius + radius + 1;
87 |
88 | int[] r = new int[wh];
89 | int[] g = new int[wh];
90 | int[] b = new int[wh];
91 | int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
92 | int[] vmin = new int[Math.max(w, h)];
93 |
94 | int divsum = (div + 1) >> 1;
95 | divsum *= divsum;
96 | int[] dv = new int[256 * divsum];
97 | for(i = 0; i<256 * divsum; i++) {
98 | dv[i] = (i / divsum);
99 | }
100 |
101 | yw = yi = 0;
102 |
103 | int[][] stack = new int[div][3];
104 | int stackpointer;
105 | int stackstart;
106 | int[] sir;
107 | int rbs;
108 | int r1 = radius + 1;
109 | int routsum, goutsum, boutsum;
110 | int rinsum, ginsum, binsum;
111 |
112 | for(y = 0; y> 16;
118 | sir[1] = (p & 0x00ff00) >> 8;
119 | sir[2] = (p & 0x0000ff);
120 | rbs = r1 - Math.abs(i);
121 | rsum += sir[0] * rbs;
122 | gsum += sir[1] * rbs;
123 | bsum += sir[2] * rbs;
124 | if(i>0) {
125 | rinsum += sir[0];
126 | ginsum += sir[1];
127 | binsum += sir[2];
128 | } else {
129 | routsum += sir[0];
130 | goutsum += sir[1];
131 | boutsum += sir[2];
132 | }
133 | }
134 | stackpointer = radius;
135 |
136 | for(x = 0; x> 16;
159 | sir[1] = (p & 0x00ff00) >> 8;
160 | sir[2] = (p & 0x0000ff);
161 |
162 | rinsum += sir[0];
163 | ginsum += sir[1];
164 | binsum += sir[2];
165 |
166 | rsum += rinsum;
167 | gsum += ginsum;
168 | bsum += binsum;
169 |
170 | stackpointer = (stackpointer + 1) % div;
171 | sir = stack[(stackpointer) % div];
172 |
173 | routsum += sir[0];
174 | goutsum += sir[1];
175 | boutsum += sir[2];
176 |
177 | rinsum -= sir[0];
178 | ginsum -= sir[1];
179 | binsum -= sir[2];
180 |
181 | yi++;
182 | }
183 | yw += w;
184 | }
185 | for(x = 0; x0) {
204 | rinsum += sir[0];
205 | ginsum += sir[1];
206 | binsum += sir[2];
207 | } else {
208 | routsum += sir[0];
209 | goutsum += sir[1];
210 | boutsum += sir[2];
211 | }
212 |
213 | if(imaxWidth) {
445 | String ellipsis = "...";
446 | int ellipsisWidth = metrics.stringWidth(ellipsis);
447 |
448 | while(textWidth + ellipsisWidth>maxWidth && !text.isEmpty()) {
449 | text = text.substring(0, text.length() - 1);
450 | textWidth = metrics.stringWidth(text);
451 | }
452 | text += ellipsis;
453 | }
454 | return text;
455 | }
456 |
457 | public static void printAvailableFonts() {
458 | // 获取系统所有可用字体名称
459 | GraphicsEnvironment e = GraphicsEnvironment.getLocalGraphicsEnvironment();
460 | String[] fontName = e.getAvailableFontFamilyNames();
461 | for(String s : fontName) {
462 | System.out.println(s);
463 | }
464 | }
465 |
466 | public static BufferedImage convertPngToJpg(BufferedImage pngImage, float quality) {
467 | BufferedImage rgbImage = new BufferedImage(pngImage.getWidth(), pngImage.getHeight(), BufferedImage.TYPE_INT_RGB);
468 | Graphics2D g = rgbImage.createGraphics();
469 | g.drawImage(pngImage, 0, 0, null);
470 | g.dispose();
471 | try(ByteArrayOutputStream os = new ByteArrayOutputStream()) {
472 | Iterator writers = ImageIO.getImageWritersByFormatName("jpg");
473 | if(!writers.hasNext()) {
474 | throw new IllegalStateException("No writers found for JPEG format.");
475 | }
476 | ImageWriter writer = writers.next();
477 | ImageWriteParam iwp = writer.getDefaultWriteParam();
478 |
479 | // 设置压缩质量
480 | iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
481 | iwp.setCompressionQuality(quality);
482 |
483 | writer.setOutput(ImageIO.createImageOutputStream(os));
484 | IIOImage iioImage = new IIOImage(rgbImage, null, null);
485 | writer.write(null, iioImage, iwp);
486 | writer.dispose();
487 |
488 | // 从字节数组中读取JPEG图像
489 | try(ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray())) {
490 | return ImageIO.read(is);
491 | }
492 | } catch(Exception e) {
493 | throw new RuntimeException("Error compressing image to JPEG format and reading it back.", e);
494 | }
495 | }
496 | }
497 |
498 |
--------------------------------------------------------------------------------
/src/main/java/com/tools/image/ImageEffect.java:
--------------------------------------------------------------------------------
1 | package com.tools.image;
2 |
3 | public class ImageEffect {
4 | private int arcW = -1;
5 | private int arcH = -1;
6 | private int blur = -1;
7 |
8 |
9 | public ImageEffect setArc(int Arc) {
10 | this.arcW = Arc;
11 | this.arcH = Arc;
12 | return this;
13 | }
14 |
15 | public int getArcW() {
16 | return arcW;
17 | }
18 |
19 | public ImageEffect setArcW(int arcW) {
20 | this.arcW = arcW;
21 | return this;
22 | }
23 |
24 | public int getArcH() {
25 | return arcH;
26 | }
27 |
28 | public ImageEffect setArcH(int arcH) {
29 | this.arcH = arcH;
30 | return this;
31 | }
32 |
33 | public int getBlur() {
34 | return blur;
35 | }
36 |
37 | public ImageEffect setBlur(int blur) {
38 | this.blur = blur;
39 | return this;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/com/tools/image/TextEffect.java:
--------------------------------------------------------------------------------
1 | package com.tools.image;
2 |
3 | public class TextEffect {
4 | private Integer maxWidth = null;
5 | private Integer spaceHeight = null;
6 |
7 | public TextEffect() {
8 | }
9 |
10 | public Integer getMaxWidth() {
11 | return maxWidth;
12 | }
13 |
14 | public TextEffect setMaxWidth(int maxWidth) {
15 | this.maxWidth = maxWidth;
16 | return this;
17 | }
18 |
19 | public Integer getSpaceHeight() {
20 | return spaceHeight;
21 | }
22 |
23 | public TextEffect setSpaceHeight(int spaceHeight) {
24 | this.spaceHeight = spaceHeight;
25 | return this;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin:
--------------------------------------------------------------------------------
1 | com.mirai.MiraiBot
--------------------------------------------------------------------------------
/src/main/resources/plugin.yml:
--------------------------------------------------------------------------------
1 | #file: noinspection YAMLSchemaValidation
2 | id: com.mirai.lin
3 | version: 0.1.0
4 | name: DanceCubeBot
5 | author: Lin
6 | dependencies: [ ] # 或者不写这行
7 | info: Just A DanceCubeBot
--------------------------------------------------------------------------------