invoke = event -> this.invokeMethod(method, event);
89 | if (!handlers.containsKey(eventClazz))
90 | handlers.put(eventClazz, new PriorityQueue<>(EventHandler::compareOrder));
91 | handlers.get(eventClazz).offer(new EventHandler(annotation, invoke));
92 | }
93 |
94 | private void invokeMethod(Method method, Event event) {
95 | Object[] paramObjs = new Object[method.getParameterCount()];
96 | paramObjs[0] = event;
97 | Parameter[] parameters = method.getParameters();
98 | for (int i = 1; i < parameters.length; i++) {
99 | Object object = factory.getBean(parameters[i].getType());
100 | paramObjs[i] = object;
101 | }
102 | try {
103 | method.invoke(bean, paramObjs);
104 | }catch (ReflectiveOperationException exception) {
105 | logger.error("执行机器人事件监听器Handler时出错,当前监听器: " + bean, exception);
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/spring-boot-itbaima-robot/src/main/java/net/itbaima/robot/listener/MessageListener.java:
--------------------------------------------------------------------------------
1 | package net.itbaima.robot.listener;
2 |
3 | import net.itbaima.robot.listener.util.TrieTree;
4 | import net.mamoe.mirai.message.data.*;
5 |
6 | import java.util.Collections;
7 | import java.util.List;
8 |
9 | /**
10 | * 消息监听器通用抽象,包含一些封装好的消息管理相关方法,比如违禁词检测(基于AC自动机实现)等
11 | *
12 | * 如果您编写的是消息监听相关的监听器,那么可以直接继承此类并获得所有
13 | * 已经封装好的工具方法,可以直接使用。
14 | *
15 | *
16 | * @since 1.0.0
17 | * @author Ketuer
18 | */
19 | public abstract class MessageListener {
20 |
21 | private TrieTree tree;
22 | private final boolean caseSensitive;
23 |
24 | /**
25 | * 构造普通的抽象消息监听器
26 | * @since 1.0.0
27 | */
28 | public MessageListener() {
29 | this(Collections.emptyList(), false);
30 | }
31 |
32 | /**
33 | * 在构造时直接填写后续用于判断的违禁词列表
34 | * @since 1.0.0
35 | * @param caseSensitive 是否大小写敏感
36 | * @param words 违禁词列表
37 | */
38 | public MessageListener(List words, boolean caseSensitive) {
39 | this.caseSensitive = caseSensitive;
40 | if(!words.isEmpty())
41 | this.setProhibitedWords(words);
42 | }
43 |
44 | /**
45 | * 设置违禁词列表,后续可以使用 {@link #invalidTextWithCount} 方法快速进行违禁词判断
46 | * @since 1.0.0
47 | * @param words 违禁词
48 | */
49 | protected void setProhibitedWords(List words){
50 | tree = new TrieTree();
51 | words.forEach(word -> tree.insert(caseSensitive ? word.toLowerCase() : word));
52 | tree.buildFailureNode();
53 | }
54 |
55 | /**
56 | * 判断一段文本中违禁词出现的次数(单个违禁词只会统计一次)
57 | * @since 1.0.0
58 | * @param text 文本内容
59 | * @return 出现违禁词次数
60 | */
61 | protected int invalidTextWithCount(String text){
62 | return tree.checkTextWithCount(caseSensitive ? text.toLowerCase() : text);
63 | }
64 |
65 | /**
66 | * 判断消息中是否出现违禁词,仅判断文本内容(单个违禁词只会统计一次)
67 | * @since 1.0.0
68 | * @param message Message
69 | * @return 出现违禁词次数
70 | */
71 | protected int invalidTextWithCount(Message message){
72 | return this.invalidTextWithCount(message.contentToString());
73 | }
74 |
75 | /**
76 | * 判断一段文本中是否出现违禁词
77 | * @since 1.0.0
78 | * @param text 文本内容
79 | * @return 是否出现
80 | */
81 | protected boolean invalidText(String text){
82 | return tree.checkText(caseSensitive ? text.toLowerCase() : text);
83 | }
84 |
85 | /**
86 | * 判断消息中是否出现违禁词,仅判断文本内容
87 | * @since 1.0.0
88 | * @param message 消息
89 | * @return 是否出现
90 | */
91 | protected boolean invalidText(Message message){
92 | return this.invalidText(message.contentToString());
93 | }
94 |
95 | /**
96 | * 撤回一条消息
97 | * @since 1.0.0
98 | * @param messages 消息
99 | */
100 | protected void recallMessage(MessageChain messages){
101 | MessageSource.recall(messages);
102 | }
103 |
104 | /**
105 | * 对一条消息进行引用,并根据给定的文本构造成一个引用回复消息
106 | * @since 1.0.0
107 | * @param quote 引用
108 | * @param message 回复内容
109 | * @return 构造完成的消息链
110 | */
111 | protected MessageChain quoteWithMessages(MessageChain quote, String... message){
112 | QuoteReply quoteReply = MessageSource.quote(quote);
113 | MessageChain chain = MessageUtils.newChain(quoteReply);
114 | for (String s : message)
115 | chain.plus(new PlainText(s));
116 | return chain;
117 | }
118 |
119 | /**
120 | * 对一条消息进行引用,并根据给定的消息链进行合并得到最终的引用回复消息
121 | * @since 1.0.0
122 | * @param quote 引用
123 | * @param messages 回复消息
124 | * @return 构造完成的消息链
125 | */
126 | protected MessageChain quoteWithMessages(MessageChain quote, Message... messages){
127 | QuoteReply quoteReply = MessageSource.quote(quote);
128 | MessageChain chain = MessageUtils.newChain(quoteReply);
129 | for (Message message : messages)
130 | chain.plus(message);
131 | return chain;
132 | }
133 |
134 | /**
135 | * 对一个用户进行@操作,并根据给定的文本构造成一个引用回复消息
136 | * @since 1.0.0
137 | * @param user 用户
138 | * @param message 回复内容
139 | * @return 构造完成的消息链
140 | */
141 | protected MessageChain atWithMessages(long user, String... message){
142 | At at = new At(user);
143 | MessageChain chain = MessageUtils.newChain(at);
144 | for (String s : message)
145 | chain.plus(new PlainText(s));
146 | return chain;
147 | }
148 |
149 | /**
150 | * 对一个用户进行@操作,并根据给定的消息链进行合并得到最终的引用回复消息
151 | * @since 1.0.0
152 | * @param user 用户
153 | * @param messages 回复消息
154 | * @return 构造完成的消息链
155 | */
156 | protected MessageChain atWithMessages(long user, Message... messages){
157 | At at = new At(user);
158 | MessageChain chain = MessageUtils.newChain(at);
159 | for (Message message : messages)
160 | chain.plus(message);
161 | return chain;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/spring-boot-itbaima-robot/src/main/java/net/itbaima/robot/listener/util/TrieTree.java:
--------------------------------------------------------------------------------
1 | package net.itbaima.robot.listener.util;
2 |
3 | import java.util.*;
4 | import java.util.regex.Pattern;
5 |
6 | /**
7 | * 内部使用,处理关键词匹配AC自动机算法
8 | */
9 | public class TrieTree {
10 | private final TrieNode root;
11 |
12 | // 静态成员,预编译的正则表达式
13 | private static final Pattern INVALID_CHAR_PATTERN = Pattern.compile("[^a-zA-Z0-9\u4E00-\u9FA5]");
14 |
15 | // 创建一个新的Trie树,根节点为空
16 | public TrieTree() {
17 | root = new TrieNode();
18 | }
19 |
20 | // 插入一个新的关键词到Trie树中
21 | public void insert(String word) {
22 | TrieNode node = root;
23 | for (char c : word.toCharArray()) {
24 | node.children.putIfAbsent(c, new TrieNode()); // 如果该字符在当前节点的子节点中不存在,则创建一个新的子节点
25 | node = node.children.get(c); // 移动到下一个子节点
26 | }
27 | node.end = true; // 标记最后一个字符的节点为结束节点,表示一个完整的关键词
28 | }
29 |
30 | // 构建AC自动机的失效链接
31 | public void buildFailureNode() {
32 | Queue queue = new LinkedList<>();
33 | for (TrieNode child : root.children.values()) {
34 | child.fail = root; // 根节点的子节点的失效链接都指向根节点
35 | queue.add(child);
36 | }
37 | while (!queue.isEmpty()) {
38 | TrieNode current = queue.poll();
39 | for (char c : current.children.keySet()) {
40 | TrieNode child = current.children.get(c);
41 | queue.add(child);
42 | TrieNode failNode = current.fail;
43 | while (failNode != null && !failNode.children.containsKey(c))
44 | failNode = failNode.fail; // 寻找失效链接的节点
45 | child.fail = failNode != null ? failNode.children.get(c) : root; // 如果找到了失效链接的节点,则指向该节点的对应子节点,否则指向根节点
46 | }
47 | }
48 | }
49 |
50 | // 检查文本中是否存在关键词
51 | public boolean checkText(String text) {
52 | TrieNode current = root;
53 | for (char c : text.toCharArray()) {
54 | if (isInvalidChar(c)) continue; // 如果字符无效,则跳过
55 | while (current != null && !current.children.containsKey(c))
56 | current = current.fail; // 如果当前节点的子节点中不存在该字符,则跟随失效链接向上查找
57 | if (current == null) {
58 | current = root; // 如果没有找到,则回到根节点并继续查找
59 | continue;
60 | }
61 | current = current.children.get(c); // 如果找到了,则转到下一个子节点
62 | if (current.end) return true; // 如果找到了一个关键词的结束节点,则返回true
63 | }
64 | return false; // 如果没有找到任何关键词,则返回false
65 | }
66 |
67 | // 检查文本中的关键词数量
68 | public int checkTextWithCount(String text) {
69 | Set nodes = new HashSet<>();
70 | int count = 0;
71 | TrieNode current = root;
72 | for (char c : text.toCharArray()) {
73 | if (isInvalidChar(c)) continue; // 无效字符直接跳过
74 | while (current != null && !current.children.containsKey(c))
75 | current = current.fail; // 如果当前节点的子节点中不存在该字符,则跟随失效链接向上查找
76 | if (current == null) {
77 | current = root; // 如果没有找到,则回到根节点并继续查找
78 | continue;
79 | }
80 | current = current.children.get(c); // 如果找到了,则转到下一个子节点
81 | TrieNode tmp = current;
82 | while (tmp != null) {
83 | if(tmp.end && !nodes.contains(tmp)) {
84 | nodes.add(tmp); // 如果找到了一个关键词的结束节点,并且该节点还没有被计数过,则计数加1
85 | count++;
86 | }
87 | tmp = tmp.fail; // 向上跟随失效链接查找其他可能的关键词
88 | }
89 | }
90 | return count; // 返回找到的关键词数量
91 | }
92 |
93 | // 检查字符是否为无效字符,这里定义了无效字符为非英文、非数字和非中文的字符
94 | private boolean isInvalidChar(char c) {
95 | return INVALID_CHAR_PATTERN.matcher(String.valueOf(c)).matches();
96 | }
97 |
98 | // Trie树的节点类,包含子节点、失效链接和结束标记
99 | private static class TrieNode {
100 | HashMap children;
101 | TrieNode fail;
102 | boolean end;
103 |
104 | public TrieNode() {
105 | children = new HashMap<>();
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/spring-boot-itbaima-robot/src/main/java/net/itbaima/robot/service/RobotService.java:
--------------------------------------------------------------------------------
1 | package net.itbaima.robot.service;
2 |
3 | import net.mamoe.mirai.Bot;
4 | import net.mamoe.mirai.contact.ContactList;
5 | import net.mamoe.mirai.contact.Friend;
6 | import net.mamoe.mirai.contact.Group;
7 | import net.mamoe.mirai.contact.NormalMember;
8 | import net.mamoe.mirai.data.UserProfile;
9 | import net.mamoe.mirai.message.MessageReceipt;
10 | import net.mamoe.mirai.message.data.Message;
11 |
12 | import java.util.function.Consumer;
13 |
14 | public interface RobotService {
15 | MessageReceipt sendMessageToFriend(long user, Message message);
16 |
17 | void run(Consumer action);
18 |
19 | void runWithFriend(long group, Consumer action);
20 |
21 | void runWithGroup(long group, Consumer action);
22 |
23 | void runWithGroupMembers(long group, Consumer> action);
24 |
25 | void runWithProfile(long user, Consumer action);
26 |
27 | MessageReceipt sendMessageToFriend(long user, String message);
28 | void deleteFriend(long user);
29 | Friend getFriend(long user);
30 | MessageReceipt sendMessageToGroup(long group, Message message);
31 | MessageReceipt sendMessageToGroup(long group, String message);
32 | void deleteGroup(long group);
33 | Group getGroup(long group);
34 | boolean isGroupContainsUser(long group, long user);
35 | }
36 |
--------------------------------------------------------------------------------
/spring-boot-itbaima-robot/src/main/java/net/itbaima/robot/service/impl/RobotServiceImpl.java:
--------------------------------------------------------------------------------
1 | package net.itbaima.robot.service.impl;
2 |
3 | import jakarta.annotation.Resource;
4 | import net.itbaima.robot.service.RobotService;
5 | import net.mamoe.mirai.Bot;
6 | import net.mamoe.mirai.IMirai;
7 | import net.mamoe.mirai.contact.ContactList;
8 | import net.mamoe.mirai.contact.Friend;
9 | import net.mamoe.mirai.contact.Group;
10 | import net.mamoe.mirai.contact.NormalMember;
11 | import net.mamoe.mirai.data.UserProfile;
12 | import net.mamoe.mirai.message.MessageReceipt;
13 | import net.mamoe.mirai.message.data.Message;
14 |
15 | import java.util.function.Consumer;
16 |
17 | /**
18 | * 可以快速使用的RobotService服务类,包含大量通用操作
19 | *
20 | * @since 1.0.0
21 | * @author Ketuer
22 | */
23 | public class RobotServiceImpl implements RobotService {
24 |
25 | @Resource
26 | private Bot bot;
27 |
28 | @Resource
29 | private IMirai mirai;
30 |
31 | /**
32 | * 在回调中进行聊天机器人相关操作
33 | * @param action 操作
34 | */
35 | @Override
36 | public void run(Consumer action) {
37 | action.accept(bot);
38 | }
39 |
40 | /**
41 | * 在回调中对指定好友进行操作
42 | * @param fried 好友QQ号
43 | * @param action 操作
44 | */
45 | @Override
46 | public void runWithFriend(long fried, Consumer action) {
47 | action.accept(this.getFriend(fried));
48 | }
49 |
50 | /**
51 | * 在回调中对指定群聊进行操作
52 | * @param group 群号
53 | * @param action 操作
54 | */
55 | @Override
56 | public void runWithGroup(long group, Consumer action) {
57 | action.accept(this.getGroup(group));
58 | }
59 |
60 | /**
61 | * 在回调中对指定群成员列表进行操作
62 | * @param group 群号
63 | * @param action 操作
64 | */
65 | @Override
66 | public void runWithGroupMembers(long group, Consumer> action) {
67 | action.accept(this.getGroup(group).getMembers());
68 | }
69 |
70 | /**
71 | * 查询用户信息,并在回调中对指定用户信息进行操作
72 | * @param user 用户QQ号
73 | * @param action 操作
74 | */
75 | @Override
76 | public void runWithProfile(long user, Consumer action){
77 | action.accept(mirai.queryProfile(bot, user));
78 | }
79 |
80 | /**
81 | * 向指定好友发送一条文本消息
82 | * @param friend 好友
83 | * @param message 文本消息
84 | * @return 消息回执
85 | */
86 | @Override
87 | public MessageReceipt sendMessageToFriend(long friend, String message){
88 | return this.findFriendById(friend).sendMessage(message);
89 | }
90 |
91 | /**
92 | * 向指定好友发送一条消息
93 | * @param friend 好友
94 | * @param message 消息
95 | * @return 消息回执
96 | */
97 | @Override
98 | public MessageReceipt sendMessageToFriend(long friend, Message message){
99 | return this.findFriendById(friend).sendMessage(message);
100 | }
101 |
102 | /**
103 | * 删除指定ID的好友
104 | * @param friend 好友
105 | */
106 | @Override
107 | public void deleteFriend(long friend){
108 | this.findFriendById(friend).delete();
109 | }
110 |
111 | /**
112 | * 获取指定ID的好友,不存在时返回null
113 | * @param friend 好友
114 | */
115 | @Override
116 | public Friend getFriend(long friend){
117 | return bot.getFriend(friend);
118 | }
119 |
120 | /**
121 | * 发送一条消息到群聊
122 | * @param group 群号
123 | * @param message 消息
124 | * @return 消息回执
125 | */
126 | @Override
127 | public MessageReceipt sendMessageToGroup(long group, Message message) {
128 | return this.findGroupById(group).sendMessage(message);
129 | }
130 |
131 | /**
132 | * 发送一条文本消息到群聊
133 | * @param group 群号
134 | * @param message 消息
135 | * @return 消息回执
136 | */
137 | @Override
138 | public MessageReceipt sendMessageToGroup(long group, String message) {
139 | return this.findGroupById(group).sendMessage(message);
140 | }
141 |
142 | /**
143 | * 退出指定ID的群聊
144 | * @param group 群号
145 | */
146 | @Override
147 | public void deleteGroup(long group) {
148 | this.findGroupById(group).quit();
149 | }
150 |
151 | /**
152 | * 获取指定ID的群里,不存在时返回null
153 | * @param group 群号
154 | * @return 群
155 | */
156 | @Override
157 | public Group getGroup(long group) {
158 | return bot.getGroup(group);
159 | }
160 |
161 | /**
162 | * 快速判断某个群内是否存在指定QQ号的用户
163 | * @param group 群号
164 | * @param user QQ号
165 | * @return 是否存在
166 | */
167 | @Override
168 | public boolean isGroupContainsUser(long group, long user) {
169 | return this.findGroupById(group).contains(user);
170 | }
171 |
172 | private Friend findFriendById(long id){
173 | return bot.getFriendOrFail(id);
174 | }
175 |
176 | private Group findGroupById(long id){
177 | return bot.getGroupOrFail(id);
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/spring-boot-itbaima-robot/src/main/java/xyz/cssxsh/mirai/tool/ProtocolVersionFixer.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.tool
2 |
3 | import kotlinx.serialization.json.*
4 | import net.mamoe.mirai.internal.utils.*
5 | import net.mamoe.mirai.utils.BotConfiguration
6 | import net.mamoe.mirai.utils.cast
7 | import net.mamoe.mirai.utils.hexToBytes
8 | import net.mamoe.mirai.utils.toUHexString
9 | import xyz.cssxsh.mirai.tool.sign.service.SignServiceFactory
10 | import java.io.File
11 | import java.net.URL
12 | import java.time.Instant
13 | import java.time.OffsetDateTime
14 | import java.time.ZoneId
15 |
16 | /**
17 | * 原名: FixProtocolVersion.
18 | */
19 | @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
20 | object ProtocolVersionFixer {
21 |
22 | private val clazz = MiraiProtocolInternal::class.java
23 |
24 | private val constructor = clazz.constructors.single()
25 |
26 | @PublishedApi
27 | internal val protocols: MutableMap by lazy {
28 | try {
29 | MiraiProtocolInternal.protocols
30 | } catch (_: NoSuchMethodError) {
31 | with(MiraiProtocolInternal) {
32 | this::class.members
33 | .first { "protocols" == it.name }
34 | .call(this)
35 | }.cast()
36 | }
37 | }
38 |
39 | @PublishedApi
40 | internal fun MiraiProtocolInternal.field(name: String, default: T): T {
41 | @Suppress("UNCHECKED_CAST")
42 | return kotlin.runCatching {
43 | val field = clazz.getDeclaredField(name)
44 | field.isAccessible = true
45 | field.get(this) as T
46 | }.getOrElse {
47 | default
48 | }
49 | }
50 |
51 | @PublishedApi
52 | internal fun MiraiProtocolInternal.change(block: MiraiProtocolInternalBuilder.() -> Unit): MiraiProtocolInternal {
53 | val builder = MiraiProtocolInternalBuilder(this).apply(block)
54 | return when (constructor.parameterCount) {
55 | 10 -> constructor.newInstance(
56 | builder.apkId,
57 | builder.id,
58 | builder.ver,
59 | builder.sdkVer,
60 | builder.miscBitMap,
61 | builder.subSigMap,
62 | builder.mainSigMap,
63 | builder.sign,
64 | builder.buildTime,
65 | builder.ssoVersion
66 | )
67 | 11 -> constructor.newInstance(
68 | builder.apkId,
69 | builder.id,
70 | builder.ver,
71 | builder.sdkVer,
72 | builder.miscBitMap,
73 | builder.subSigMap,
74 | builder.mainSigMap,
75 | builder.sign,
76 | builder.buildTime,
77 | builder.ssoVersion,
78 | builder.supportsQRLogin
79 | )
80 | else -> this
81 | } as MiraiProtocolInternal
82 | }
83 |
84 | @PublishedApi
85 | internal class MiraiProtocolInternalBuilder(impl: MiraiProtocolInternal) {
86 | var apkId: String = impl.field("apkId", "")
87 | var id: Long = impl.field("id", 0)
88 | var ver: String = impl.field("ver", "")
89 | var sdkVer: String = impl.field("sdkVer", "")
90 | var miscBitMap: Int = impl.field("miscBitMap", 0)
91 | var subSigMap: Int = impl.field("subSigMap", 0)
92 | var mainSigMap: Int = impl.field("mainSigMap", 0)
93 | var sign: String = impl.field("sign", "")
94 | var buildTime: Long = impl.field("buildTime", 0)
95 | var ssoVersion: Int = impl.field("ssoVersion", 0)
96 | var supportsQRLogin: Boolean = impl.field("supportsQRLogin", false)
97 | }
98 |
99 | /**
100 | * fix-protocol-version 插件最初的功能
101 | *
102 | * 根据本地代码检查协议版本更新
103 | */
104 | @JvmStatic
105 | fun update() {
106 | protocols.compute(BotConfiguration.MiraiProtocol.ANDROID_PHONE) { _, impl ->
107 | when {
108 | null == impl -> null
109 | impl.runCatching { id }.isFailure -> impl.change {
110 | apkId = "com.tencent.mobileqq"
111 | id = 537163098
112 | ver = "8.9.58.11170"
113 | sdkVer = "6.0.0.2545"
114 | miscBitMap = 0x08F7_FF7C
115 | subSigMap = 0x0001_0400
116 | mainSigMap = 0x0214_10E0
117 | sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D"
118 | buildTime = 1684467300L
119 | ssoVersion = 20
120 | }
121 | impl.id < 537163098 -> impl.apply {
122 | apkId = "com.tencent.mobileqq"
123 | id = 537163098
124 | ver = "8.9.58"
125 | buildVer = "8.9.58.11170"
126 | sdkVer = "6.0.0.2545"
127 | miscBitMap = 0x08F7_FF7C
128 | subSigMap = 0x0001_0400
129 | mainSigMap = 0x0214_10E0
130 | sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D"
131 | buildTime = 1684467300L
132 | ssoVersion = 20
133 | appKey = "0S200MNJT807V3GE"
134 | supportsQRLogin = false
135 | }
136 | else -> impl
137 | }
138 | }
139 | protocols.compute(BotConfiguration.MiraiProtocol.ANDROID_PAD) { _, impl ->
140 | when {
141 | null == impl -> null
142 | impl.runCatching { id }.isFailure -> impl.change {
143 | apkId = "com.tencent.mobileqq"
144 | id = 537161402
145 | ver = "8.9.58.11170"
146 | sdkVer = "6.0.0.2545"
147 | miscBitMap = 0x08F7_FF7C
148 | subSigMap = 0x0001_0400
149 | mainSigMap = 0x0214_10E0
150 | sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D"
151 | buildTime = 1684467300L
152 | ssoVersion = 20
153 | }
154 | impl.id < 537161402 -> impl.apply {
155 | apkId = "com.tencent.mobileqq"
156 | id = 537161402
157 | ver = "8.9.58"
158 | buildVer = "8.9.58.11170"
159 | sdkVer = "6.0.0.2545"
160 | miscBitMap = 0x08F7_FF7C
161 | subSigMap = 0x0001_0400
162 | mainSigMap = 0x0214_10E0
163 | sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D"
164 | buildTime = 1684467300L
165 | ssoVersion = 20
166 | appKey = "0S200MNJT807V3GE"
167 | supportsQRLogin = false
168 | }
169 | else -> impl
170 | }
171 | }
172 | protocols.compute(BotConfiguration.MiraiProtocol.ANDROID_WATCH) { _, impl ->
173 | when {
174 | null == impl -> null
175 | impl.runCatching { id }.isFailure -> impl.change {
176 | apkId = "com.tencent.qqlite"
177 | id = 537065138
178 | ver = "2.0.8"
179 | sdkVer = "6.0.0.2365"
180 | miscBitMap = 0x00F7_FF7C
181 | subSigMap = 0x0001_0400
182 | mainSigMap = 0x00FF_32F2
183 | sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D"
184 | buildTime = 1559564731L
185 | ssoVersion = 5
186 | supportsQRLogin = true
187 | }
188 | impl.id < 537065138 -> impl.apply {
189 | apkId = "com.tencent.qqlite"
190 | id = 537065138
191 | ver = "2.0.8"
192 | buildVer = "2.0.8"
193 | sdkVer = "6.0.0.2365"
194 | miscBitMap = 0x00F7_FF7C
195 | subSigMap = 0x0001_0400
196 | mainSigMap = 0x00FF_32F2
197 | sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D"
198 | buildTime = 1559564731L
199 | ssoVersion = 5
200 | supportsQRLogin = true
201 | }
202 | else -> impl
203 | }
204 | }
205 | protocols.compute(BotConfiguration.MiraiProtocol.IPAD) { _, impl ->
206 | when {
207 | null == impl -> null
208 | impl.runCatching { id }.isFailure -> impl.change {
209 | apkId = "com.tencent.minihd.qq"
210 | id = 537155074
211 | ver = "8.9.50.611"
212 | sdkVer = "6.0.0.2535"
213 | miscBitMap = 0x08F7_FF7C
214 | subSigMap = 0x0001_0400
215 | mainSigMap = 0x001E_10E0
216 | sign = "AA 39 78 F4 1F D9 6F F9 91 4A 66 9E 18 64 74 C7"
217 | buildTime = 1676531414L
218 | ssoVersion = 19
219 | }
220 | impl.id < 537155074 -> impl.apply {
221 | apkId = "com.tencent.minihd.qq"
222 | id = 537155074
223 | ver = "8.9.50"
224 | buildVer = "8.9.50.611"
225 | sdkVer = "6.0.0.2535"
226 | miscBitMap = 0x08F7_FF7C
227 | subSigMap = 0x0001_0400
228 | mainSigMap = 0x001E_10E0
229 | sign = "AA 39 78 F4 1F D9 6F F9 91 4A 66 9E 18 64 74 C7"
230 | buildTime = 1676531414L
231 | ssoVersion = 19
232 | appKey = "0S200MNJT807V3GE"
233 | supportsQRLogin = false
234 | }
235 | else -> impl
236 | }
237 | }
238 | protocols.compute(BotConfiguration.MiraiProtocol.MACOS) { _, impl ->
239 | when {
240 | null == impl -> null
241 | impl.runCatching { id }.isFailure -> impl.change {
242 | id = 537128930
243 | ver = "6.8.2.21241"
244 | buildTime = 1647227495
245 | }
246 | impl.id < 537128930 -> impl.apply {
247 | id = 537128930
248 | ver = "6.8.2"
249 | buildVer = "6.8.2.21241"
250 | buildTime = 1647227495
251 | }
252 | else -> impl
253 | }
254 | }
255 | }
256 |
257 | /**
258 | * 从 [RomiChan/protocol-versions](https://github.com/RomiChan/protocol-versions) 获取指定版本协议
259 | *
260 | * @since 1.9.6
261 | */
262 | @JvmStatic
263 | fun fetch(protocol: BotConfiguration.MiraiProtocol, version: String) {
264 | if(this.existsLocalFile(protocol)) {
265 | this.load(protocol)
266 | } else {
267 | val (file, url) = when (protocol) {
268 | BotConfiguration.MiraiProtocol.ANDROID_PHONE -> {
269 | File(SignServiceFactory.workDir + "android_phone.json") to
270 | when (version) {
271 | "", "latest" -> URL("https://raw.githubusercontent.com/RomiChan/protocol-versions/master/android_phone.json")
272 | else -> URL("https://raw.githubusercontent.com/RomiChan/protocol-versions/master/android_phone/${version}.json")
273 | }
274 | }
275 | BotConfiguration.MiraiProtocol.ANDROID_PAD -> {
276 | File(SignServiceFactory.workDir + "android_pad.json") to
277 | when (version) {
278 | "", "latest" -> URL("https://raw.githubusercontent.com/RomiChan/protocol-versions/master/android_pad.json")
279 | else -> URL("https://raw.githubusercontent.com/RomiChan/protocol-versions/master/android_pad/${version}.json")
280 | }
281 | }
282 | else -> throw IllegalArgumentException("不支持同步的协议: ${protocol.name}")
283 | }
284 |
285 | val json: JsonObject = kotlin.runCatching {
286 | url.openConnection()
287 | .apply {
288 | connectTimeout = 30_000
289 | readTimeout = 30_000
290 | }
291 | .getInputStream().use { it.readBytes() }
292 | .decodeToString()
293 | }.recoverCatching { throwable ->
294 | try {
295 | URL("https://ghproxy.com/$url").openConnection()
296 | .apply {
297 | connectTimeout = 30_000
298 | readTimeout = 30_000
299 | }
300 | .getInputStream().use { it.readBytes() }
301 | .decodeToString()
302 | } catch (cause: Throwable) {
303 | val exception = throwable.cause as? java.net.UnknownHostException ?: throwable
304 | exception.addSuppressed(cause)
305 | throw exception
306 | }
307 | }.fold(
308 | onSuccess = { text ->
309 | val online = Json.parseToJsonElement(text).jsonObject
310 | check(online.getValue("app_id").jsonPrimitive.long != 0L) { "载入的 ${protocol.name.lowercase()}.json 有误" }
311 | file.writeText(text)
312 | file.setLastModified(online.getValue("build_time").jsonPrimitive.long * 1000)
313 | online
314 | },
315 | onFailure = { cause ->
316 | throw IllegalStateException("从 $url 下载协议失败", cause)
317 | }
318 | )
319 |
320 | store(protocol, json)
321 | }
322 | }
323 |
324 | /**
325 | * 判断本地是否存在已经下载好的协议
326 | */
327 | @JvmStatic
328 | private fun existsLocalFile(protocol: BotConfiguration.MiraiProtocol) : Boolean{
329 | return File(SignServiceFactory.workDir + "${protocol.name.lowercase()}.json").exists()
330 | }
331 |
332 | /**
333 | * 从本地加载协议
334 | *
335 | * @since 1.8.0
336 | */
337 | @JvmStatic
338 | private fun load(protocol: BotConfiguration.MiraiProtocol) {
339 | val file = File(SignServiceFactory.workDir + "${protocol.name.lowercase()}.json")
340 | val json: JsonObject = Json.parseToJsonElement(file.readText()).jsonObject
341 | store(protocol, json)
342 | }
343 |
344 | @JvmStatic
345 | private fun store(protocol: BotConfiguration.MiraiProtocol, json: JsonObject) {
346 | check(json.getValue("app_id").jsonPrimitive.long != 0L) { "载入的 ${protocol.name.lowercase()}.json 有误" }
347 | protocols.compute(protocol) { _, impl ->
348 | when {
349 | null == impl -> null
350 | impl.runCatching { id }.isFailure -> impl.change {
351 | apkId = json.getValue("apk_id").jsonPrimitive.content
352 | id = json.getValue("app_id").jsonPrimitive.long
353 | ver = json.getValue("sort_version_name").jsonPrimitive.content
354 | sdkVer = json.getValue("sdk_version").jsonPrimitive.content
355 | miscBitMap = json.getValue("misc_bitmap").jsonPrimitive.int
356 | subSigMap = json.getValue("sub_sig_map").jsonPrimitive.int
357 | mainSigMap = json.getValue("main_sig_map").jsonPrimitive.int
358 | sign = json.getValue("apk_sign").jsonPrimitive.content.hexToBytes().toUHexString(" ")
359 | buildTime = json.getValue("build_time").jsonPrimitive.long
360 | ssoVersion = json.getValue("sso_version").jsonPrimitive.int
361 | }
362 | else -> impl.apply {
363 | apkId = json.getValue("apk_id").jsonPrimitive.content
364 | id = json.getValue("app_id").jsonPrimitive.long
365 | buildVer = json.getValue("sort_version_name").jsonPrimitive.content
366 | ver = buildVer.substringBeforeLast(".")
367 | sdkVer = json.getValue("sdk_version").jsonPrimitive.content
368 | miscBitMap = json.getValue("misc_bitmap").jsonPrimitive.int
369 | subSigMap = json.getValue("sub_sig_map").jsonPrimitive.int
370 | mainSigMap = json.getValue("main_sig_map").jsonPrimitive.int
371 | sign = json.getValue("apk_sign").jsonPrimitive.content.hexToBytes().toUHexString(" ")
372 | buildTime = json.getValue("build_time").jsonPrimitive.long
373 | ssoVersion = json.getValue("sso_version").jsonPrimitive.int
374 | appKey = json.getValue("app_key").jsonPrimitive.content
375 | }
376 | }
377 | }
378 | }
379 |
380 | /**
381 | * 协议版本信息
382 | *
383 | * @since 1.6.0
384 | */
385 | @JvmStatic
386 | fun info(): Map {
387 | return protocols.mapValues { (protocol, info) ->
388 | val version = info.field("buildVer", null as String?) ?: info.field("ver", "???")
389 | val epochSecond = info.field("buildTime", 0L)
390 | val datetime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(epochSecond), ZoneId.systemDefault())
391 |
392 | "%-13s %-12s %s".format(protocol, version, datetime)
393 | }
394 | }
395 | }
396 |
--------------------------------------------------------------------------------
/spring-boot-itbaima-robot/src/main/java/xyz/cssxsh/mirai/tool/sign/service/SignServiceConfig.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.tool.sign.service
2 |
3 | import kotlinx.serialization.ExperimentalSerializationApi
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 | import kotlinx.serialization.json.JsonNames
7 |
8 | @Serializable
9 | @OptIn(ExperimentalSerializationApi::class)
10 | data class SignServiceConfig(
11 | @SerialName("base_url")
12 | val base: String,
13 | @SerialName("type")
14 | val type: String = "",
15 | @SerialName("key")
16 | val key: String = "",
17 | @SerialName("server_identity_key")
18 | @JsonNames("serverIdentityKey")
19 | val serverIdentityKey: String = "",
20 | @SerialName("authorization_key")
21 | @JsonNames("authorizationKey")
22 | val authorizationKey: String = ""
23 | )
24 |
--------------------------------------------------------------------------------
/spring-boot-itbaima-robot/src/main/java/xyz/cssxsh/mirai/tool/sign/service/SignServiceFactory.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.tool.sign.service
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.job
5 | import net.mamoe.mirai.internal.spi.EncryptService
6 | import net.mamoe.mirai.internal.spi.EncryptServiceContext
7 | import net.mamoe.mirai.internal.utils.*
8 | import net.mamoe.mirai.utils.BotConfiguration
9 | import net.mamoe.mirai.utils.MiraiLogger
10 | import net.mamoe.mirai.utils.Services
11 | import xyz.cssxsh.mirai.tool.sign.service.impl.MagicSignerGuide
12 | import xyz.cssxsh.mirai.tool.sign.service.impl.UnidbgFetchQsign
13 | import java.net.ConnectException
14 | import java.net.URL
15 |
16 | /**
17 | * 原名: KFCFactory
18 | */
19 | class SignServiceFactory : EncryptService.Factory {
20 |
21 | companion object {
22 | @JvmStatic
23 | internal var signServiceConfig: SignServiceConfig? = null
24 |
25 | @JvmStatic
26 | internal var workDir: String = ""
27 |
28 | @JvmStatic
29 | internal val logger: MiraiLogger = MiraiLogger.Factory.create(SignServiceFactory::class)
30 |
31 | @JvmStatic
32 | fun install() {
33 | Services.register(
34 | EncryptService.Factory::class.qualifiedName!!,
35 | SignServiceFactory::class.qualifiedName!!,
36 | ::SignServiceFactory
37 | )
38 | }
39 |
40 | @JvmStatic
41 | internal val created: MutableSet = java.util.concurrent.ConcurrentHashMap.newKeySet()
42 |
43 | @JvmStatic
44 | fun initConfiguration(path: String, config: SignServiceConfig) {
45 | workDir = "$path/"
46 | signServiceConfig = config
47 | }
48 | }
49 |
50 | override fun createForBot(context: EncryptServiceContext, serviceSubScope: CoroutineScope): EncryptService {
51 | if (created.add(context.id).not()) {
52 | throw UnsupportedOperationException("repeated create EncryptService")
53 | }
54 | serviceSubScope.coroutineContext.job.invokeOnCompletion {
55 | created.remove(context.id)
56 | }
57 | try {
58 | org.asynchttpclient.Dsl.config()
59 | } catch (cause: NoClassDefFoundError) {
60 | throw RuntimeException("请参照 https://search.maven.org/artifact/org.asynchttpclient/async-http-client/2.12.3/jar 添加依赖", cause)
61 | }
62 | return when (val protocol = context.extraArgs[EncryptServiceContext.KEY_BOT_PROTOCOL]) {
63 | BotConfiguration.MiraiProtocol.ANDROID_PHONE, BotConfiguration.MiraiProtocol.ANDROID_PAD -> {
64 | @Suppress("INVISIBLE_MEMBER")
65 | val version = MiraiProtocolInternal[protocol].ver
66 | val config = signServiceConfig!!
67 | when (val type = config.type.ifEmpty { throw IllegalArgumentException("need server type") }) {
68 | "fuqiuluo/unidbg-fetch-qsign", "fuqiuluo", "unidbg-fetch-qsign" -> {
69 | try {
70 | val about = URL(config.base).readText()
71 | logger.info("unidbg-fetch-qsign by ${config.base} about " + about.replace("\n", "").replace(" ", ""))
72 | when {
73 | "version" !in about -> {
74 | // 低于等于 1.1.3 的的版本 requestToken 不工作
75 | System.setProperty(UnidbgFetchQsign.REQUEST_TOKEN_INTERVAL, "0")
76 | logger.warning("请更新 unidbg-fetch-qsign")
77 | }
78 | version !in about -> {
79 | throw IllegalStateException("unidbg-fetch-qsign by ${config.base} 的版本与 ${protocol}(${version}) 似乎不匹配")
80 | }
81 | }
82 | } catch (cause: ConnectException) {
83 | throw RuntimeException("请检查 unidbg-fetch-qsign by ${config.base} 的可用性", cause)
84 | } catch (cause: java.io.FileNotFoundException) {
85 | throw RuntimeException("请检查 unidbg-fetch-qsign by ${config.base} 的可用性", cause)
86 | }
87 | UnidbgFetchQsign(
88 | server = config.base,
89 | key = config.key,
90 | coroutineContext = serviceSubScope.coroutineContext
91 | )
92 | }
93 | "kiliokuara/magic-signer-guide", "kiliokuara", "magic-signer-guide", "vivo50" -> {
94 | try {
95 | val about = URL(config.base).readText()
96 | logger.info("magic-signer-guide by ${config.base} about \n" + about)
97 | when {
98 | "void" == about.trim() -> {
99 | logger.warning("请更新 magic-signer-guide 的 docker 镜像")
100 | }
101 | version !in about -> {
102 | throw IllegalStateException("magic-signer-guide by ${config.base} 与 ${protocol}(${version}) 似乎不匹配")
103 | }
104 | }
105 | } catch (cause: ConnectException) {
106 | throw RuntimeException("请检查 magic-signer-guide by ${config.base} 的可用性", cause)
107 | } catch (cause: java.io.FileNotFoundException) {
108 | throw RuntimeException("请检查 unidbg-fetch-qsign by ${config.base} 的可用性", cause)
109 | }
110 | MagicSignerGuide(
111 | server = config.base,
112 | serverIdentityKey = config.serverIdentityKey,
113 | authorizationKey = config.authorizationKey,
114 | coroutineContext = serviceSubScope.coroutineContext
115 | )
116 | }
117 | else -> throw UnsupportedOperationException(type)
118 | }
119 | }
120 |
121 | else -> throw UnsupportedOperationException(protocol.name)
122 | }
123 | }
124 |
125 | override fun toString(): String {
126 | return "EncryptServiceFactory(config=$signServiceConfig)"
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/spring-boot-itbaima-robot/src/main/java/xyz/cssxsh/mirai/tool/sign/service/impl/MagicSignerGuide.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.tool.sign.service.impl
2 |
3 | import kotlinx.coroutines.*
4 | import kotlinx.serialization.DeserializationStrategy
5 | import kotlinx.serialization.SerialName
6 | import kotlinx.serialization.Serializable
7 | import kotlinx.serialization.builtins.ListSerializer
8 | import kotlinx.serialization.builtins.serializer
9 | import kotlinx.serialization.json.*
10 | import net.mamoe.mirai.Bot
11 | import net.mamoe.mirai.event.broadcast
12 | import net.mamoe.mirai.event.events.BotOfflineEvent
13 | import net.mamoe.mirai.internal.spi.EncryptService
14 | import net.mamoe.mirai.internal.spi.EncryptServiceContext
15 | import net.mamoe.mirai.internal.utils.*
16 | import net.mamoe.mirai.utils.*
17 | import org.asynchttpclient.DefaultAsyncHttpClientConfig
18 | import org.asynchttpclient.Dsl
19 | import org.asynchttpclient.ListenableFuture
20 | import org.asynchttpclient.Response
21 | import org.asynchttpclient.ws.WebSocket
22 | import org.asynchttpclient.ws.WebSocketListener
23 | import org.asynchttpclient.ws.WebSocketUpgradeHandler
24 | import java.security.KeyFactory
25 | import java.security.KeyPair
26 | import java.security.KeyPairGenerator
27 | import java.security.Signature
28 | import java.security.spec.X509EncodedKeySpec
29 | import java.util.*
30 | import java.util.concurrent.CompletableFuture
31 | import java.util.concurrent.ConcurrentHashMap
32 | import java.util.concurrent.TimeUnit
33 | import java.util.concurrent.TimeoutException
34 | import javax.crypto.Cipher
35 | import javax.crypto.spec.SecretKeySpec
36 | import kotlin.coroutines.CoroutineContext
37 |
38 | /**
39 | * kiliokuara/magic-signer-guide 的加密服务实现.
40 | * 原名: ViVo50.
41 | */
42 | class MagicSignerGuide(
43 | private val server: String,
44 | private val serverIdentityKey: String,
45 | private val authorizationKey: String,
46 | coroutineContext: CoroutineContext
47 | ) : EncryptService, CoroutineScope {
48 |
49 | companion object {
50 | @JvmStatic
51 | internal val logger: MiraiLogger = MiraiLogger.Factory.create(MagicSignerGuide::class)
52 |
53 | @JvmStatic
54 | val SESSION_EXCEPT_TIMEOUT: String = "xyz.cssxsh.mirai.tool.sign.service.impl.MagicSignerGuide.Session.timeout"
55 | }
56 |
57 | override val coroutineContext: CoroutineContext =
58 | coroutineContext + SupervisorJob(coroutineContext[Job]) + CoroutineExceptionHandler { context, exception ->
59 | when (exception) {
60 | is CancellationException -> {
61 | // ...
62 | }
63 | else -> {
64 | logger.warning({ "with ${context[CoroutineName]}" }, exception)
65 | }
66 | }
67 | }
68 |
69 | private val client = Dsl.asyncHttpClient(
70 | DefaultAsyncHttpClientConfig.Builder()
71 | .setKeepAlive(true)
72 | .setUserAgent("curl/7.61.0")
73 | .setRequestTimeout(30_000)
74 | .setConnectTimeout(30_000)
75 | .setReadTimeout(180_000)
76 | )
77 |
78 | private val sharedKey = SecretKeySpec(UUID.randomUUID().toString().substring(0, 16).encodeToByteArray(), "AES")
79 |
80 | private val rsaKeyPair: KeyPair = KeyPairGenerator.getInstance("RSA")
81 | .apply { initialize(4096) }
82 | .generateKeyPair()
83 |
84 | private val sessions = ConcurrentHashMap()
85 |
86 | private val white = ConcurrentHashMap.newKeySet()
87 |
88 | private fun ListenableFuture.getBody(deserializer: DeserializationStrategy): T {
89 | val response = get()
90 | return Json.decodeFromString(deserializer, response.responseBody)
91 | }
92 |
93 | private fun EncryptServiceContext.session(): Session {
94 | return sessions[id] ?: throw NoSuchElementException("Session(bot=${id})")
95 | }
96 |
97 | override fun initialize(context: EncryptServiceContext) {
98 | val device = context.extraArgs[EncryptServiceContext.KEY_DEVICE_INFO]
99 | val qimei36 = context.extraArgs[EncryptServiceContext.KEY_QIMEI36]
100 | val protocol = context.extraArgs[EncryptServiceContext.KEY_BOT_PROTOCOL]
101 | val cache = context.extraArgs[EncryptServiceContext.KEY_BOT_CACHING_DIR]
102 | val channel = context.extraArgs[EncryptServiceContext.KEY_CHANNEL_PROXY]
103 |
104 | logger.info("Bot(${context.id}) initialize by $server")
105 |
106 | val cmd = java.io.File(cache, "cmd.txt")
107 |
108 | sessions.computeIfAbsent(context.id) {
109 | val token = handshake(uin = context.id)
110 | val session = Session(token = token, bot = context.id, channel = channel)
111 | session.websocket()
112 | coroutineContext.job.invokeOnCompletion {
113 | sessions.remove(context.id, session)
114 | session.close()
115 | }
116 | try {
117 | if (cmd.exists()) {
118 | white.addAll(cmd.readText().split("\n"))
119 | }
120 | } catch (cause: Throwable) {
121 | logger.warning("Session(bot=${context.id}) cmd_white_list cache read fail", cause)
122 | cmd.delete()
123 | }
124 | session
125 | }.apply {
126 | sendCommand(type = "rpc.initialize", deserializer = JsonElement.serializer()) {
127 | putJsonObject("extArgs") {
128 | put("KEY_QIMEI36", qimei36)
129 | putJsonObject("BOT_PROTOCOL") {
130 | putJsonObject("protocolValue") {
131 | @Suppress("INVISIBLE_MEMBER")
132 | put("ver", MiraiProtocolInternal[protocol].ver)
133 | }
134 | }
135 | }
136 | putJsonObject("device") {
137 | put("display", device.display.toUHexString(""))
138 | put("product", device.product.toUHexString(""))
139 | put("device", device.device.toUHexString(""))
140 | put("board", device.board.toUHexString(""))
141 | put("brand", device.brand.toUHexString(""))
142 | put("model", device.model.toUHexString(""))
143 | put("bootloader", device.bootloader.toUHexString(""))
144 | put("fingerprint", device.fingerprint.toUHexString(""))
145 | put("bootId", device.bootId.toUHexString(""))
146 | put("procVersion", device.procVersion.toUHexString(""))
147 | put("baseBand", device.baseBand.toUHexString(""))
148 | putJsonObject("version") {
149 | put("incremental", device.version.incremental.toUHexString(""))
150 | put("release", device.version.release.toUHexString(""))
151 | put("codename", device.version.codename.toUHexString(""))
152 | put("sdk", device.version.sdk)
153 | }
154 | put("simInfo", device.simInfo.toUHexString(""))
155 | put("osType", device.osType.toUHexString(""))
156 | put("macAddress", device.macAddress.toUHexString(""))
157 | put("wifiBSSID", device.wifiBSSID.toUHexString(""))
158 | put("wifiSSID", device.wifiSSID.toUHexString(""))
159 | put("imsiMd5", device.imsiMd5.toUHexString(""))
160 | put("imei", device.imei)
161 | put("apn", device.apn.toUHexString(""))
162 | put("androidId", device.androidId.toUHexString(""))
163 | @OptIn(MiraiInternalApi::class)
164 | put("guid", device.guid.toUHexString(""))
165 | }
166 | }
167 | sendCommand(type = "rpc.get_cmd_white_list", deserializer = ListSerializer(String.serializer())).also {
168 | val list = checkNotNull(it) { "get_cmd_white_list is null" }
169 | white.clear()
170 | white.addAll(list)
171 | }
172 | }
173 |
174 | launch(CoroutineName(name = "Session(bot=${context.id})")) {
175 | cmd.writeText(white.joinToString("\n"))
176 | }
177 |
178 | logger.info("Bot(${context.id}) initialize complete")
179 | }
180 |
181 | private fun handshake(uin: Long): String {
182 | val config = client.prepareGet("${server}/service/rpc/handshake/config")
183 | .execute().getBody(HandshakeConfig.serializer())
184 |
185 | val pKeyRsaSha1 = (serverIdentityKey + config.publicKey)
186 | .toByteArray().sha1().toUHexString("").lowercase()
187 | val clientKeySignature = (pKeyRsaSha1 + serverIdentityKey)
188 | .toByteArray().sha1().toUHexString("").lowercase()
189 |
190 | check(clientKeySignature == config.keySignature) {
191 | "请检查 serverIdentityKey 是否正确。(client calculated key signature doesn't match the server provides.)"
192 | }
193 |
194 | val secret = buildJsonObject {
195 | put("authorizationKey", authorizationKey)
196 | put("sharedKey", sharedKey.encoded.decodeToString())
197 | put("botid", uin)
198 | }.let {
199 | val text = Json.encodeToString(JsonElement.serializer(), it)
200 | val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
201 | val publicKey = KeyFactory.getInstance("RSA")
202 | .generatePublic(X509EncodedKeySpec(Base64.getDecoder().decode(config.publicKey)))
203 | cipher.init(Cipher.ENCRYPT_MODE, publicKey)
204 |
205 | cipher.doFinal(text.encodeToByteArray())
206 | }
207 |
208 |
209 | val result = client.preparePost("${server}/service/rpc/handshake/handshake")
210 | .setBody(Json.encodeToString(JsonElement.serializer(), buildJsonObject {
211 | put("clientRsa", Base64.getEncoder().encodeToString(rsaKeyPair.public.encoded))
212 | put("secret", Base64.getEncoder().encodeToString(secret))
213 | }))
214 | .execute().getBody(HandshakeResult.serializer())
215 |
216 | check(result.status == 200) { result.reason }
217 |
218 | return Base64.getDecoder().decode(result.token).decodeToString()
219 | }
220 |
221 | private fun signature(): Pair {
222 | val current = System.currentTimeMillis().toString()
223 | val privateSignature = Signature.getInstance("SHA256withRSA")
224 | privateSignature.initSign(rsaKeyPair.private)
225 | privateSignature.update(current.encodeToByteArray())
226 |
227 | return current to Base64.getEncoder().encodeToString(privateSignature.sign())
228 | }
229 |
230 | override fun encryptTlv(context: EncryptServiceContext, tlvType: Int, payload: ByteArray): ByteArray? {
231 | val command = context.extraArgs[EncryptServiceContext.KEY_COMMAND_STR]
232 |
233 | val response = context.session().sendCommand(type = "rpc.tlv", deserializer = String.serializer()) {
234 | put("tlvType", tlvType)
235 | putJsonObject("extArgs") {
236 | put("KEY_COMMAND_STR", command)
237 | }
238 | put("content", payload.toUHexString(""))
239 | } ?: return null
240 |
241 | return response.hexToBytes()
242 | }
243 |
244 | override fun qSecurityGetSign(
245 | context: EncryptServiceContext,
246 | sequenceId: Int,
247 | commandName: String,
248 | payload: ByteArray
249 | ): EncryptService.SignResult? {
250 | if (white.isEmpty().not() && commandName !in white) return null
251 |
252 | logger.debug("Bot(${context.id}) sign $commandName")
253 |
254 | val response = context.session().sendCommand(type = "rpc.sign", deserializer = RpcSignResult.serializer()) {
255 | put("seqId", sequenceId)
256 | put("command", commandName)
257 | putJsonObject("extArgs") {
258 | // ...
259 | }
260 | put("content", payload.toUHexString(""))
261 | } ?: return null
262 |
263 | return EncryptService.SignResult(
264 | sign = response.sign.hexToBytes(),
265 | extra = response.extra.hexToBytes(),
266 | token = response.token.hexToBytes(),
267 | )
268 | }
269 |
270 | override fun toString(): String {
271 | return "MagicSignerGuideService(server=${server}, sessions=${sessions.keys})"
272 | }
273 |
274 | private inner class Session(val bot: Long, val token: String, val channel: EncryptService.ChannelProxy) :
275 | WebSocketListener, AutoCloseable {
276 | private var websocket0: WebSocket? = null
277 | private var cause0: Throwable? = null
278 | private val packet: MutableMap> = ConcurrentHashMap()
279 | private val timeout: Long = System.getProperty(SESSION_EXCEPT_TIMEOUT, "60000").toLong()
280 |
281 | override fun onOpen(websocket: WebSocket) {
282 | websocket0 = websocket
283 | cause0 = null
284 | logger.info("Session(bot=${bot}) opened")
285 | }
286 |
287 | override fun onClose(websocket: WebSocket, code: Int, reason: String?) {
288 | websocket0 = null
289 | if (code != 1_000) logger.warning("Session(bot=${bot}) closed, $code - $reason")
290 | }
291 |
292 | override fun onError(cause: Throwable) {
293 | when (val websocket = websocket0) {
294 | null -> {
295 | cause0 = cause
296 | }
297 | else -> {
298 | websocket0 = null
299 | val wrapper = IllegalStateException("Session(bot=${bot}) error", cause)
300 | if (websocket.isOpen) {
301 | try {
302 | websocket.sendCloseFrame()
303 | } catch (cause: Throwable) {
304 | wrapper.addSuppressed(cause)
305 | }
306 | }
307 | logger.error("Session(bot=${bot}) error", wrapper)
308 | }
309 | }
310 | }
311 |
312 | override fun onBinaryFrame(payload: ByteArray, finalFragment: Boolean, rsv: Int) {
313 | val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
314 | cipher.init(Cipher.DECRYPT_MODE, sharedKey)
315 | val text = cipher.doFinal(payload).decodeToString()
316 |
317 | val json = Json.parseToJsonElement(text).jsonObject
318 | val id = json["packetId"]!!.jsonPrimitive.content
319 | packet.remove(id)?.complete(json)
320 |
321 | when (json["packetType"]?.jsonPrimitive?.content) {
322 | "rpc.service.send" -> {
323 | val uin = json["botUin"]!!.jsonPrimitive.long
324 | val cmd = json["command"]!!.jsonPrimitive.content
325 | launch(CoroutineName(name = "Session(bot=${bot})")) {
326 | logger.verbose("Bot(${bot}) sendMessage <- $cmd")
327 |
328 | val result = channel.sendMessage(
329 | remark = json["remark"]!!.jsonPrimitive.content,
330 | commandName = cmd,
331 | uin = uin,
332 | data = json["data"]!!.jsonPrimitive.content.hexToBytes()
333 | )
334 |
335 | if (result == null) {
336 | logger.debug("Bot(${bot}) ChannelResult is null")
337 | return@launch
338 | }
339 | logger.verbose("Bot(${bot}) sendMessage -> ${result.cmd}")
340 |
341 | sendPacket(type = "rpc.service.send", id = id) {
342 | put("command", result.cmd)
343 | put("data", result.data.toUHexString(""))
344 | }
345 | }
346 | }
347 | "service.interrupt" -> {
348 | logger.error("Session(bot=${bot}) $text")
349 | }
350 | else -> {
351 | // ...
352 | }
353 | }
354 | }
355 |
356 | private fun open(): WebSocket {
357 | val (timestamp, signature) = signature()
358 | return client.prepareGet("${server}/service/rpc/session".replace("http", "ws"))
359 | .addHeader("Authorization", token)
360 | .addHeader("X-SEC-Time", timestamp)
361 | .addHeader("X-SEC-Signature", signature)
362 | .execute(
363 | WebSocketUpgradeHandler
364 | .Builder()
365 | .addWebSocketListener(this)
366 | .build()
367 | )
368 | .get() ?: throw IllegalStateException("Session(bot=${bot}) open fail", cause0)
369 | }
370 |
371 | private fun check(): WebSocket? {
372 | val (timestamp, signature) = signature()
373 | val response = client.prepareGet("${server}/service/rpc/session/check")
374 | .addHeader("Authorization", token)
375 | .addHeader("X-SEC-Time", timestamp)
376 | .addHeader("X-SEC-Signature", signature)
377 | .execute().get()
378 |
379 | return when (response.statusCode) {
380 | 204 -> websocket0
381 | 404 -> null
382 | else -> {
383 | sessions.remove(bot, this)
384 | val cause = IllegalStateException("Session(bot=${bot}) ${response.responseBody}")
385 | launch(CoroutineName(name = "Dropped(bot=${bot})")) {
386 | @OptIn(MiraiInternalApi::class)
387 | BotOfflineEvent.Dropped(
388 | bot = Bot.getInstance(qq = bot),
389 | cause = cause
390 | ).broadcast()
391 | }
392 | throw cause
393 | }
394 | }
395 | }
396 |
397 | private fun delete() {
398 | val (timestamp, signature) = signature()
399 | val response = client.prepareDelete("${server}/service/rpc/session")
400 | .addHeader("Authorization", token)
401 | .addHeader("X-SEC-Time", timestamp)
402 | .addHeader("X-SEC-Signature", signature)
403 | .execute().get()
404 |
405 | when (response.statusCode) {
406 | 204 -> websocket0 = null
407 | 404 -> throw NoSuchElementException("Session(bot=${bot}) ${response.responseBody}")
408 | else -> throw IllegalStateException("Session(bot=${bot}) ${response.responseBody}")
409 | }
410 | }
411 |
412 | override fun close() {
413 | try {
414 | delete()
415 | } catch (cause: NoSuchElementException) {
416 | logger.warning(cause)
417 | } catch (cause: Throwable) {
418 | logger.error(cause)
419 | }
420 | try {
421 | websocket0?.takeIf { it.isOpen }?.sendCloseFrame()
422 | } catch (cause: Throwable) {
423 | logger.error(cause)
424 | }
425 | }
426 |
427 | @Synchronized
428 | fun websocket(): WebSocket {
429 | coroutineContext.ensureActive()
430 | return check() ?: open()
431 | }
432 |
433 | fun sendPacket(type: String, id: String, block: JsonObjectBuilder.() -> Unit) {
434 | val packet = buildJsonObject {
435 | put("packetId", id)
436 | put("packetType", type)
437 | block.invoke(this)
438 | }
439 | val text = Json.encodeToString(JsonElement.serializer(), packet)
440 | val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
441 | cipher.init(Cipher.ENCRYPT_MODE, sharedKey)
442 | websocket().sendBinaryFrame(cipher.doFinal(text.encodeToByteArray()))
443 | }
444 |
445 | fun sendCommand(
446 | type: String,
447 | deserializer: DeserializationStrategy,
448 | block: JsonObjectBuilder.() -> Unit = {}
449 | ): T? {
450 |
451 | val uuid = UUID.randomUUID().toString()
452 | val future = CompletableFuture()
453 | packet[uuid] = future
454 |
455 | sendPacket(type = type, id = uuid, block = block)
456 |
457 | val json = try {
458 | future.get(timeout, TimeUnit.MILLISECONDS)
459 | } catch (cause: TimeoutException) {
460 | logger.warning("Session(bot=${bot}) $type timeout ${timeout}ms", cause)
461 | return null
462 | }
463 |
464 | json["message"]?.let {
465 | throw IllegalStateException("Session(bot=${bot}) $type error: $it")
466 | }
467 |
468 | val response = json["response"] ?: return null
469 |
470 | return Json.decodeFromJsonElement(deserializer, response)
471 | }
472 |
473 | override fun toString(): String {
474 | return "Session(bot=${bot}, token=${token})"
475 | }
476 | }
477 | }
478 |
479 | @Serializable
480 | private data class HandshakeConfig(
481 | @SerialName("publicKey")
482 | val publicKey: String,
483 | @SerialName("timeout")
484 | val timeout: Long,
485 | @SerialName("keySignature")
486 | val keySignature: String
487 | )
488 |
489 | @Serializable
490 | private data class HandshakeResult(
491 | @SerialName("status")
492 | val status: Int,
493 | @SerialName("reason")
494 | val reason: String = "",
495 | @SerialName("token")
496 | val token: String = ""
497 | )
498 |
499 | @Serializable
500 | private data class RpcSignResult(
501 | @SerialName("sign")
502 | val sign: String,
503 | @SerialName("token")
504 | val token: String,
505 | @SerialName("extra")
506 | val extra: String,
507 | )
508 |
--------------------------------------------------------------------------------
/spring-boot-itbaima-robot/src/main/java/xyz/cssxsh/mirai/tool/sign/service/impl/UnidbgFetchQsign.kt:
--------------------------------------------------------------------------------
1 | package xyz.cssxsh.mirai.tool.sign.service.impl
2 |
3 | import kotlinx.coroutines.*
4 | import kotlinx.serialization.ExperimentalSerializationApi
5 | import kotlinx.serialization.SerialName
6 | import kotlinx.serialization.Serializable
7 | import kotlinx.serialization.builtins.ListSerializer
8 | import kotlinx.serialization.builtins.serializer
9 | import kotlinx.serialization.json.Json
10 | import kotlinx.serialization.json.JsonElement
11 | import kotlinx.serialization.json.JsonNames
12 | import net.mamoe.mirai.Bot
13 | import net.mamoe.mirai.event.broadcast
14 | import net.mamoe.mirai.event.events.BotOfflineEvent
15 | import net.mamoe.mirai.internal.spi.EncryptService
16 | import net.mamoe.mirai.internal.spi.EncryptServiceContext
17 | import net.mamoe.mirai.utils.*
18 | import org.asynchttpclient.DefaultAsyncHttpClientConfig
19 | import org.asynchttpclient.Dsl
20 | import kotlin.coroutines.CoroutineContext
21 |
22 | /**
23 | * fuqiuluo/unidbg-fetch-qsign 的加密服务实现.
24 | */
25 | class UnidbgFetchQsign(private val server: String, private val key: String, coroutineContext: CoroutineContext) :
26 | EncryptService, CoroutineScope {
27 |
28 | companion object {
29 | @JvmStatic
30 | internal val logger: MiraiLogger = MiraiLogger.Factory.create(UnidbgFetchQsign::class)
31 |
32 | @JvmStatic
33 | val REQUEST_TOKEN_INTERVAL: String = "xyz.cssxsh.mirai.tool.sign.service.impl.UnidbgFetchQsign.token.interval"
34 |
35 | @JvmStatic
36 | internal val CMD_WHITE_LIST = UnidbgFetchQsign::class.java.getResource("/cmd.txt")!!.readText().lines()
37 | }
38 |
39 | override val coroutineContext: CoroutineContext =
40 | coroutineContext + SupervisorJob(coroutineContext[Job]) + CoroutineExceptionHandler { context, exception ->
41 | when (exception) {
42 | is CancellationException -> {
43 | // ...
44 | }
45 | else -> {
46 | logger.warning({ "with ${context[CoroutineName]}" }, exception)
47 | }
48 | }
49 | }
50 |
51 | private val client = Dsl.asyncHttpClient(
52 | DefaultAsyncHttpClientConfig.Builder()
53 | .setKeepAlive(true)
54 | .setUserAgent("curl/7.61.0")
55 | .setRequestTimeout(90_000)
56 | .setConnectTimeout(30_000)
57 | .setReadTimeout(180_000)
58 | )
59 |
60 | private var channel0: EncryptService.ChannelProxy? = null
61 |
62 | private val channel: EncryptService.ChannelProxy get() = channel0 ?: throw IllegalStateException("need initialize")
63 |
64 | private val token = java.util.concurrent.atomic.AtomicLong(0)
65 |
66 | override fun initialize(context: EncryptServiceContext) {
67 | val device = context.extraArgs[EncryptServiceContext.KEY_DEVICE_INFO]
68 | val qimei36 = context.extraArgs[EncryptServiceContext.KEY_QIMEI36]
69 | val channel = context.extraArgs[EncryptServiceContext.KEY_CHANNEL_PROXY]
70 |
71 | logger.info("Bot(${context.id}) initialize by $server")
72 |
73 | channel0 = channel
74 |
75 | if (token.get() == 0L) {
76 | val uin = context.id
77 | @OptIn(MiraiInternalApi::class)
78 | register(
79 | uin = uin,
80 | androidId = device.androidId.decodeToString(),
81 | guid = device.guid.toUHexString(),
82 | qimei36 = qimei36
83 | )
84 | coroutineContext.job.invokeOnCompletion {
85 | try {
86 | destroy(uin = uin)
87 | } catch (cause : Throwable) {
88 | logger.warning("Bot(${uin}) destroy", cause)
89 | } finally {
90 | token.compareAndSet(uin, 0)
91 | }
92 | }
93 | }
94 |
95 | logger.info("Bot(${context.id}) initialize complete")
96 | }
97 |
98 | private fun register(uin: Long, androidId: String, guid: String, qimei36: String) {
99 | val response = client.prepareGet("${server}/register")
100 | .addQueryParam("uin", uin.toString())
101 | .addQueryParam("android_id", androidId)
102 | .addQueryParam("guid", guid)
103 | .addQueryParam("qimei36", qimei36)
104 | .addQueryParam("key", key)
105 | .execute().get()
106 | val body = Json.decodeFromString(DataWrapper.serializer(), response.responseBody)
107 | body.check(uin = uin)
108 |
109 | logger.info("Bot(${uin}) register, ${body.message}")
110 | }
111 |
112 | private fun destroy(uin: Long) {
113 | val response = client.prepareGet("${server}/destroy")
114 | .addQueryParam("uin", uin.toString())
115 | .addQueryParam("key", key)
116 | .execute().get()
117 | if (response.statusCode == 404) return
118 | val body = Json.decodeFromString(DataWrapper.serializer(), response.responseBody)
119 |
120 | logger.info("Bot(${uin}) destroy, ${body.message}")
121 | }
122 |
123 | private fun DataWrapper.check(uin: Long) {
124 | if (code == 0) return
125 | token.compareAndSet(uin, 0)
126 | val cause = IllegalStateException("unidbg-fetch-qsign 服务异常, 请检查其日志, $message")
127 | launch(CoroutineName(name = "Dropped(${uin})")) {
128 | if ("Uin is not registered." != message) return@launch
129 | @OptIn(MiraiInternalApi::class)
130 | BotOfflineEvent.Dropped(
131 | bot = Bot.getInstance(qq = uin),
132 | cause = cause
133 | ).broadcast()
134 | }
135 | throw cause
136 | }
137 |
138 | override fun encryptTlv(context: EncryptServiceContext, tlvType: Int, payload: ByteArray): ByteArray? {
139 | if (tlvType != 0x544) return null
140 | val command = context.extraArgs[EncryptServiceContext.KEY_COMMAND_STR]
141 |
142 | val data = customEnergy(uin = context.id, salt = payload, data = command)
143 |
144 | return data.hexToBytes()
145 | }
146 |
147 | private fun customEnergy(uin: Long, salt: ByteArray, data: String): String {
148 | val response = client.prepareGet("${server}/custom_energy")
149 | .addQueryParam("uin", uin.toString())
150 | .addQueryParam("salt", salt.toUHexString(""))
151 | .addQueryParam("data", data)
152 | .execute().get()
153 | val body = Json.decodeFromString(DataWrapper.serializer(), response.responseBody)
154 | body.check(uin = uin)
155 |
156 | logger.debug("Bot(${uin}) custom_energy ${data}, ${body.message}")
157 |
158 | return Json.decodeFromJsonElement(String.serializer(), body.data)
159 | }
160 |
161 | override fun qSecurityGetSign(
162 | context: EncryptServiceContext,
163 | sequenceId: Int,
164 | commandName: String,
165 | payload: ByteArray
166 | ): EncryptService.SignResult? {
167 | if (commandName == "StatSvc.register") {
168 | if (token.compareAndSet(0, context.id)) {
169 | val uin = context.id
170 | launch(CoroutineName(name = "RequestToken")) {
171 | while (isActive) {
172 | val interval = System.getProperty(REQUEST_TOKEN_INTERVAL, "2400000").toLong()
173 | if (interval <= 0L) break
174 | if (interval < 600_000) logger.warning("$REQUEST_TOKEN_INTERVAL=${interval} < 600_000 (ms)")
175 | delay(interval)
176 | val request = try {
177 | requestToken(uin = uin)
178 | } catch (cause: Throwable) {
179 | logger.error(cause)
180 | continue
181 | }
182 | callback(uin = uin, request = request)
183 | }
184 | }
185 | }
186 | }
187 |
188 | if (commandName !in CMD_WHITE_LIST) return null
189 |
190 | val data = sign(uin = context.id, cmd = commandName, seq = sequenceId, buffer = payload)
191 |
192 | callback(uin = context.id, request = data.request)
193 |
194 | return EncryptService.SignResult(
195 | sign = data.sign.hexToBytes(),
196 | token = data.token.hexToBytes(),
197 | extra = data.extra.hexToBytes()
198 | )
199 | }
200 |
201 | private fun sign(uin: Long, cmd: String, seq: Int, buffer: ByteArray): SignResult {
202 | val response = client.preparePost("${server}/sign")
203 | .addFormParam("uin", uin.toString())
204 | .addFormParam("cmd", cmd)
205 | .addFormParam("seq", seq.toString())
206 | .addFormParam("buffer", buffer.toUHexString(""))
207 | .execute().get()
208 | val body = Json.decodeFromString(DataWrapper.serializer(), response.responseBody)
209 | body.check(uin = uin)
210 |
211 | logger.debug("Bot(${uin}) sign ${cmd}, ${body.message}")
212 |
213 | return Json.decodeFromJsonElement(SignResult.serializer(), body.data)
214 | }
215 |
216 | private fun requestToken(uin: Long): List {
217 | val response = client.prepareGet("${server}/request_token")
218 | .addQueryParam("uin", uin.toString())
219 | .execute().get()
220 | val body = Json.decodeFromString(DataWrapper.serializer(), response.responseBody)
221 | body.check(uin = uin)
222 |
223 | logger.info("Bot(${uin}) request_token, ${body.message}")
224 |
225 | return Json.decodeFromJsonElement(ListSerializer(RequestCallback.serializer()), body.data)
226 | }
227 |
228 | private fun submit(uin: Long, cmd: String, callbackId: Long, buffer: ByteArray) {
229 | val response = client.prepareGet("${server}/submit")
230 | .addQueryParam("uin", uin.toString())
231 | .addQueryParam("cmd", cmd)
232 | .addQueryParam("callback_id", callbackId.toString())
233 | .addQueryParam("buffer", buffer.toUHexString(""))
234 | .execute().get()
235 | val body = Json.decodeFromString(DataWrapper.serializer(), response.responseBody)
236 | body.check(uin = uin)
237 |
238 | logger.debug("Bot(${uin}) submit ${cmd}, ${body.message}")
239 | }
240 |
241 | private fun callback(uin: Long, request: List) {
242 | launch(CoroutineName(name = "SendMessage")) {
243 | for (callback in request) {
244 | logger.debug("Bot(${uin}) sendMessage ${callback.cmd} ")
245 | val result = try {
246 | channel.sendMessage(
247 | remark = "mobileqq.msf.security",
248 | commandName = callback.cmd,
249 | uin = 0,
250 | data = callback.body.hexToBytes()
251 | )
252 | } catch (cause: Throwable) {
253 | throw RuntimeException("Bot(${uin}) callback ${callback.cmd}", cause)
254 | }
255 | if (result == null) {
256 | logger.debug("Bot(${uin}) callback ${callback.cmd} ChannelResult is null")
257 | continue
258 | }
259 |
260 | submit(uin = uin, cmd = result.cmd, callbackId = callback.id, buffer = result.data)
261 | }
262 | }
263 | }
264 |
265 | override fun toString(): String {
266 | return "UnidbgFetchQsignService(server=${server}, uin=${token})"
267 | }
268 |
269 |
270 | }
271 |
272 | @Serializable
273 | private data class DataWrapper(
274 | @SerialName("code")
275 | val code: Int = 0,
276 | @SerialName("msg")
277 | val message: String = "",
278 | @SerialName("data")
279 | val data: JsonElement
280 | )
281 |
282 | @Serializable
283 | private data class SignResult(
284 | @SerialName("token")
285 | val token: String = "",
286 | @SerialName("extra")
287 | val extra: String = "",
288 | @SerialName("sign")
289 | val sign: String = "",
290 | @SerialName("o3did")
291 | val o3did: String = "",
292 | @SerialName("requestCallback")
293 | val request: List = emptyList()
294 | )
295 |
296 | @Serializable
297 | private data class RequestCallback(
298 | @SerialName("body")
299 | val body: String,
300 | @SerialName("callback_id")
301 | @OptIn(ExperimentalSerializationApi::class)
302 | @JsonNames("callbackId", "callback_id")
303 | val id: Long,
304 | @SerialName("cmd")
305 | val cmd: String
306 | )
307 |
--------------------------------------------------------------------------------
/spring-boot-itbaima-robot/src/main/resources/cmd.txt:
--------------------------------------------------------------------------------
1 | OidbSvcTrpcTcp.0x55f_0
2 | OidbSvcTrpcTcp.0x1100_1
3 | qidianservice.269
4 | OidbSvc.0x4ff_9_IMCore
5 | MsgProxy.SendMsg
6 | SQQzoneSvc.shuoshuo
7 | OidbSvc.0x758_1
8 | QChannelSvr.trpc.qchannel.commwriter.ComWriter.DoReply
9 | trpc.login.ecdh.EcdhService.SsoNTLoginPasswordLoginUnusualDevice
10 | wtlogin.device_lock
11 | OidbSvc.0x758_0
12 | wtlogin_device.tran_sim_emp
13 | OidbSvc.0x4ff_9
14 | trpc.springfestival.redpacket.LuckyBag.SsoSubmitGrade
15 | FeedCloudSvr.trpc.feedcloud.commwriter.ComWriter.DoReply
16 | trpc.o3.report.Report.SsoReport
17 | SQQzoneSvc.addReply
18 | OidbSvc.0x8a1_7
19 | QChannelSvr.trpc.qchannel.commwriter.ComWriter.DoComment
20 | OidbSvcTrpcTcp.0xf67_1
21 | friendlist.ModifyGroupInfoReq
22 | OidbSvcTrpcTcp.0xf65_1
23 | OidbSvcTrpcTcp.0xf65_10
24 | OidbSvcTrpcTcp.0xf65_10
25 | OidbSvcTrpcTcp.0xf67_5
26 | OidbSvc.0x56c_6
27 | OidbSvc.0x8ba
28 | SQQzoneSvc.like
29 | OidbSvcTrpcTcp.0xf88_1
30 | OidbSvc.0x8a1_0
31 | wtlogin.name2uin
32 | SQQzoneSvc.addComment
33 | wtlogin.login
34 | trpc.o3.ecdh_access.EcdhAccess.SsoSecureA2Access
35 | OidbSvcTrpcTcp.0x101e_2
36 | qidianservice.135
37 | FeedCloudSvr.trpc.feedcloud.commwriter.ComWriter.DoComment
38 | FeedCloudSvr.trpc.feedcloud.commwriter.ComWriter.DoBarrage
39 | OidbSvcTrpcTcp.0x101e_1
40 | OidbSvc.0x89a_0
41 | friendlist.addFriend
42 | ProfileService.GroupMngReq
43 | OidbSvc.oidb_0x758
44 | MessageSvc.PbSendMsg
45 | FeedCloudSvr.trpc.feedcloud.commwriter.ComWriter.DoLike
46 | OidbSvc.0x758
47 | trpc.o3.ecdh_access.EcdhAccess.SsoSecureA2Establish
48 | FeedCloudSvr.trpc.feedcloud.commwriter.ComWriter.DoPush
49 | qidianservice.290
50 | trpc.qlive.relationchain_svr.RelationchainSvr.Follow
51 | trpc.o3.ecdh_access.EcdhAccess.SsoSecureAccess
52 | FeedCloudSvr.trpc.feedcloud.commwriter.ComWriter.DoFollow
53 | SQQzoneSvc.forward
54 | ConnAuthSvr.sdk_auth_api
55 | wtlogin.qrlogin
56 | wtlogin.register
57 | OidbSvcTrpcTcp.0x6d9_4
58 | trpc.passwd.manager.PasswdManager.SetPasswd
59 | friendlist.AddFriendReq
60 | qidianservice.207
61 | ProfileService.getGroupInfoReq
62 | OidbSvcTrpcTcp.0x1107_1
63 | OidbSvcTrpcTcp.0x1105_1
64 | SQQzoneSvc.publishmood
65 | wtlogin.exchange_emp
66 | OidbSvc.0x88d_0
67 | wtlogin_device.login
68 | OidbSvcTrpcTcp.0xfa5_1
69 | trpc.qqhb.qqhb_proxy.Handler.sso_handle
70 | OidbSvcTrpcTcp.0xf89_1
71 | OidbSvc.0x9fa
72 | FeedCloudSvr.trpc.feedcloud.commwriter.ComWriter.PublishFeed
73 | QChannelSvr.trpc.qchannel.commwriter.ComWriter.PublishFeed
74 | OidbSvcTrpcTcp.0xf57_106
75 | ConnAuthSvr.sdk_auth_api_emp
76 | OidbSvcTrpcTcp.0xf6e_1
77 | trpc.qlive.word_svr.WordSvr.NewPublicChat
78 | trpc.passwd.manager.PasswdManager.VerifyPasswd
79 | trpc.group_pro.msgproxy.sendmsg
80 | OidbSvc.0x89b_1
81 | OidbSvcTrpcTcp.0xf57_9
82 | FeedCloudSvr.trpc.videocircle.circleprofile.CircleProfile.SetProfile
83 | OidbSvc.0x6d9_4
84 | OidbSvcTrpcTcp.0xf55_1
85 | ConnAuthSvr.fast_qq_login
86 | OidbSvcTrpcTcp.0xf57_1
87 | trpc.o3.ecdh_access.EcdhAccess.SsoEstablishShareKey
88 | wtlogin.trans_emp
--------------------------------------------------------------------------------
/spring-boot-starter-itbaima-robot/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 |
8 | net.itbaima
9 | itbaima-robot-starter
10 | 1.0.2
11 |
12 | spring-boot-starter-itbaima-robot
13 |
14 |
15 | 17
16 | 17
17 | UTF-8
18 |
19 |
20 |
21 |
22 | net.itbaima
23 | spring-boot-itbaima-robot
24 | 1.0.2
25 |
26 |
27 |
28 | net.itbaima
29 | spring-boot-autoconfigure-itbaima-robot
30 | 1.0.2
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------