@alemonjs/qq-bot 2.1.36 → 2.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -1979
- package/lib/config.d.ts +5 -0
- package/lib/framework-actions.d.ts +75 -0
- package/lib/framework-actions.js +620 -0
- package/lib/framework-events.d.ts +10 -0
- package/lib/framework-events.js +182 -0
- package/lib/hook.d.ts +16 -12
- package/lib/hook.js +5 -9
- package/lib/index.d.ts +6 -1
- package/lib/index.js +2 -1
- package/lib/index.websoket.js +6 -2
- package/lib/message/AUDIO_EVENT.d.ts +20 -0
- package/lib/message/AUDIO_EVENT.js +1 -0
- package/lib/message/FORUM_EVENT.d.ts +33 -0
- package/lib/message/FORUM_EVENT.js +1 -0
- package/lib/message/GUILD_AUDIT.d.ts +14 -0
- package/lib/message/GUILD_AUDIT.js +1 -0
- package/lib/message/INTERACTION_CREATE.d.ts +13 -20
- package/lib/message/MESSAGE_CREATE.d.ts +3 -9
- package/lib/message/MESSAGE_DELETE.d.ts +15 -1
- package/lib/message/group/C2C_MESSAGE_CREATE.d.ts +3 -13
- package/lib/message/group/C2C_MSG_RECEIVE.d.ts +1 -1
- package/lib/message/group/C2C_MSG_REJECT.d.ts +1 -1
- package/lib/message/group/FRIEND_ADD.d.ts +7 -1
- package/lib/message/group/FRIEND_DEL.d.ts +4 -1
- package/lib/message/group/GROUP_AT_MESSAGE_CREATE.d.ts +6 -27
- package/lib/message/group/GROUP_MEMBER_ADD.d.ts +4 -2
- package/lib/message/group/GROUP_MEMBER_REMOVE.d.ts +4 -2
- package/lib/message/group/GROUP_MESSAGE_CREATE.d.ts +6 -27
- package/lib/message/group/GROUP_MSG_RECEIVE.d.ts +1 -1
- package/lib/message/group/GROUP_MSG_REJECT.d.ts +1 -1
- package/lib/message/group/MESSAGE_AUDIT_PASS.d.ts +2 -0
- package/lib/message/group/MESSAGE_AUDIT_REJECT.d.ts +2 -0
- package/lib/message/group/SUBSCRIBE_MESSAGE_STATUS.d.ts +17 -0
- package/lib/message/group/SUBSCRIBE_MESSAGE_STATUS.js +1 -0
- package/lib/register.d.ts +1 -0
- package/lib/register.js +287 -115
- package/lib/sdk/api.d.ts +93 -14
- package/lib/sdk/api.js +241 -45
- package/lib/sdk/client.webhook.js +39 -19
- package/lib/sdk/client.websoket.js +2 -1
- package/lib/sdk/instance.d.ts +1 -6
- package/lib/sdk/instance.js +20 -66
- package/lib/sdk/intents.d.ts +1 -0
- package/lib/sdk/intents.js +1 -0
- package/lib/sdk/message.d.ts +1 -1
- package/lib/sdk/message.group.d.ts +4 -2
- package/lib/sdk/message.guild.d.ts +19 -0
- package/lib/sdk/non-channel-types.d.ts +138 -0
- package/lib/sdk/non-channel-types.js +1 -0
- package/lib/sdk/registry.js +2 -2
- package/lib/sdk/typing.d.ts +13 -1
- package/lib/sdk/webhook.secret.d.ts +1 -1
- package/lib/sdk/webhook.secret.js +10 -5
- package/lib/sends.d.ts +3 -3
- package/lib/sends.js +46 -10
- package/lib/text-sanitizer.d.ts +13 -0
- package/lib/text-sanitizer.js +26 -0
- package/lib/upload.js +13 -37
- package/package.json +8 -6
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { getIdentity } from "./config.js";
|
|
2
|
+
import { C2C_MESSAGE_CREATE, GROUP_AT_MESSAGE_CREATE } from "./sends.js";
|
|
3
|
+
import { normalizeJoinRequest } from "./framework-events.js";
|
|
4
|
+
import { ResultCode, createResult } from "alemonjs";
|
|
5
|
+
|
|
6
|
+
//#region src/framework-actions.ts
|
|
7
|
+
const page = (items, raw) => ({
|
|
8
|
+
Items: items,
|
|
9
|
+
NextCursor: raw.next_cursor,
|
|
10
|
+
HasMore: raw.is_end === void 0 ? Boolean(raw.next_cursor) : !raw.is_end
|
|
11
|
+
});
|
|
12
|
+
const pagination = (params) => ({
|
|
13
|
+
cursor: params?.Cursor ?? params?.After,
|
|
14
|
+
limit: params?.Limit
|
|
15
|
+
});
|
|
16
|
+
const memberIds = (ids) => {
|
|
17
|
+
if (!Array.isArray(ids) || ids.length < 1 || ids.length > 20 || ids.some((id) => typeof id !== "string" || !id)) throw new Error("Expected 1 to 20 non-empty member IDs");
|
|
18
|
+
return ids;
|
|
19
|
+
};
|
|
20
|
+
const nonEmptyIds = (ids, maximum, name) => {
|
|
21
|
+
if (!Array.isArray(ids) || ids.length < 1 || ids.length > maximum || ids.some((id) => typeof id !== "string" || !id)) throw new Error(`Expected 1 to ${maximum} non-empty ${name}`);
|
|
22
|
+
return ids;
|
|
23
|
+
};
|
|
24
|
+
const textLength = (value) => Buffer.byteLength(value, "utf8");
|
|
25
|
+
const menuItem = (item, child = false) => {
|
|
26
|
+
const maximum = child ? 14 : 10;
|
|
27
|
+
if (!item.name || textLength(item.name) > maximum) throw new Error(`Menu item name must be at most ${maximum} bytes`);
|
|
28
|
+
if (child && item.type === "menu") throw new Error("Menu sub-items cannot contain another menu");
|
|
29
|
+
if (item.type === "link" && !item.url.startsWith("https://")) throw new Error("Menu links must use https");
|
|
30
|
+
if (item.type === "menu" && (item.items.length < 1 || item.items.length > 5)) throw new Error("A menu must contain 1 to 5 sub-items");
|
|
31
|
+
return {
|
|
32
|
+
name: item.name,
|
|
33
|
+
type: item.type === "message" ? "send_message" : item.type,
|
|
34
|
+
..."text" in item && { send_message: item.text },
|
|
35
|
+
..."url" in item && { link: item.url },
|
|
36
|
+
..."items" in item && { sub_menu_items: item.items.map((value) => menuItem(value, true)) },
|
|
37
|
+
..."id" in item && { switch: {
|
|
38
|
+
switch_id: item.id,
|
|
39
|
+
default: item.default
|
|
40
|
+
} }
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const validatePanel = (panel) => {
|
|
44
|
+
if (panel.items.length > 20 || panel.remark && textLength(panel.remark) > 255) throw new Error("Panel supports at most 20 items and a 255-byte remark");
|
|
45
|
+
for (const item of panel.items) {
|
|
46
|
+
if (!item.name || textLength(item.name) > 14 || item.description && textLength(item.description) > 30) throw new Error("Panel item name/description exceeds its limit");
|
|
47
|
+
if (item.type === "link" && (!item.url || !item.url.startsWith("https://"))) throw new Error("Panel link items require an https URL");
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const validatePanelAudience = (scope, audience, userIds, guildIds) => {
|
|
51
|
+
if (!["group", "c2c"].includes(scope)) throw new Error("QQ panel scope must be group or c2c");
|
|
52
|
+
if (Boolean(userIds?.length) && Boolean(guildIds?.length)) throw new Error("A panel audience cannot mix users and groups");
|
|
53
|
+
if (audience === "specific") nonEmptyIds(scope === "group" ? guildIds : userIds, 20, scope === "group" ? "group IDs" : "user IDs");
|
|
54
|
+
else if (userIds?.length || guildIds?.length) throw new Error("Panel audience IDs require audience: specific");
|
|
55
|
+
if (scope === "group" && userIds?.length || scope === "c2c" && guildIds?.length) throw new Error("Panel audience IDs do not match its scope");
|
|
56
|
+
};
|
|
57
|
+
const removal = (r) => ({
|
|
58
|
+
status: r.remove_members_result,
|
|
59
|
+
failedBlacklistIds: r.add_to_member_blacklist_fail_openids ?? []
|
|
60
|
+
});
|
|
61
|
+
const blacklistResult = (r) => ({ failedUserIds: r.fail_openids ?? [] });
|
|
62
|
+
const operation = (value) => value === "remove" ? "del" : "add";
|
|
63
|
+
const mediaType = (value) => ({
|
|
64
|
+
image: 1,
|
|
65
|
+
video: 2,
|
|
66
|
+
audio: 3,
|
|
67
|
+
file: 4
|
|
68
|
+
})[value];
|
|
69
|
+
const mediaReceipt = (r) => ({
|
|
70
|
+
fileId: r.file_info,
|
|
71
|
+
uuid: r.file_uuid,
|
|
72
|
+
ttl: r.ttl,
|
|
73
|
+
messageId: r.id,
|
|
74
|
+
url: r.raw_url
|
|
75
|
+
});
|
|
76
|
+
const receipt = (r) => ({
|
|
77
|
+
id: r.id,
|
|
78
|
+
timestamp: r.timestamp,
|
|
79
|
+
referenceId: r.ext_info?.ref_idx
|
|
80
|
+
});
|
|
81
|
+
const policy = (r) => ({
|
|
82
|
+
id: r.strategy_id,
|
|
83
|
+
guildIds: r.group_openids,
|
|
84
|
+
guildNumbers: r.group_ids,
|
|
85
|
+
enabled: r.is_enable === "on",
|
|
86
|
+
expiresAt: r.expire_at,
|
|
87
|
+
remark: r.remark,
|
|
88
|
+
whitelistCount: r.whitelist_user_count,
|
|
89
|
+
createdAt: r.created_at,
|
|
90
|
+
updatedAt: r.updated_at
|
|
91
|
+
});
|
|
92
|
+
const policyBody = (p) => ({
|
|
93
|
+
group_openids: p.guildIds,
|
|
94
|
+
group_ids: p.guildNumbers,
|
|
95
|
+
is_enable: p.enabled === void 0 ? void 0 : p.enabled ? "on" : "off",
|
|
96
|
+
expire_at: p.expiresAt,
|
|
97
|
+
remark: p.remark
|
|
98
|
+
});
|
|
99
|
+
const fromMenuItem = (item) => ({
|
|
100
|
+
name: item.name,
|
|
101
|
+
...item.type === "send_message" ? {
|
|
102
|
+
type: "message",
|
|
103
|
+
text: item.send_message
|
|
104
|
+
} : item.type === "switch" ? {
|
|
105
|
+
type: "switch",
|
|
106
|
+
id: item.switch?.switch_id,
|
|
107
|
+
default: item.switch?.default
|
|
108
|
+
} : item.type === "menu" ? {
|
|
109
|
+
type: "menu",
|
|
110
|
+
items: item.sub_menu_items?.map(fromMenuItem) ?? []
|
|
111
|
+
} : {
|
|
112
|
+
type: "link",
|
|
113
|
+
url: item.link
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
const panelBody = (panel) => ({
|
|
117
|
+
items: panel.items.map((item) => ({
|
|
118
|
+
name: item.name,
|
|
119
|
+
desc: item.description,
|
|
120
|
+
type: item.type,
|
|
121
|
+
only_admin: item.adminOnly,
|
|
122
|
+
link: item.url
|
|
123
|
+
})),
|
|
124
|
+
remark: panel.remark,
|
|
125
|
+
version: panel.version
|
|
126
|
+
});
|
|
127
|
+
const fromPanel = (r) => {
|
|
128
|
+
if (!["group", "c2c"].includes(r.scope)) throw new Error(`QQ panel scope ${r.scope || "unknown"} is outside the non-channel framework surface`);
|
|
129
|
+
return {
|
|
130
|
+
id: r.panel_id,
|
|
131
|
+
scope: r.scope,
|
|
132
|
+
audience: r.target_type,
|
|
133
|
+
userIds: r.user_openids,
|
|
134
|
+
guildIds: r.group_openids,
|
|
135
|
+
version: r.version,
|
|
136
|
+
createdAt: r.created_at,
|
|
137
|
+
updatedAt: r.updated_at,
|
|
138
|
+
panel: {
|
|
139
|
+
items: (r.panel?.items ?? []).map((item) => ({
|
|
140
|
+
name: item.name,
|
|
141
|
+
description: item.desc,
|
|
142
|
+
type: item.type,
|
|
143
|
+
adminOnly: item.only_admin,
|
|
144
|
+
url: item.link
|
|
145
|
+
})),
|
|
146
|
+
remark: r.panel?.remark,
|
|
147
|
+
version: r.panel?.version
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
const member = (r, guildId) => {
|
|
152
|
+
const [IsMaster, UserKey] = getIdentity(r.member_openid);
|
|
153
|
+
return {
|
|
154
|
+
UserId: r.member_openid,
|
|
155
|
+
UserName: r.username,
|
|
156
|
+
UserKey,
|
|
157
|
+
IsMaster,
|
|
158
|
+
IsBot: r.bot ?? false,
|
|
159
|
+
GuildId: guildId,
|
|
160
|
+
Role: r.member_role,
|
|
161
|
+
Roles: r.member_role ? [r.member_role] : [],
|
|
162
|
+
UnionId: r.union_openid,
|
|
163
|
+
JoinedAt: r.joined_at ? Date.parse(r.joined_at) : void 0
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
const toQQMessage = (content) => {
|
|
167
|
+
if ([
|
|
168
|
+
content.markdown,
|
|
169
|
+
content.media,
|
|
170
|
+
content.card
|
|
171
|
+
].filter(Boolean).length > 1) throw new Error("markdown, media and card are mutually exclusive message bodies");
|
|
172
|
+
if (content.keyboard?.templateId && content.keyboard.rows) throw new Error("keyboard templateId and rows are mutually exclusive");
|
|
173
|
+
return {
|
|
174
|
+
msg_type: content.markdown ? 2 : content.media ? 7 : content.card ? 3 : 0,
|
|
175
|
+
content: content.markdown ? void 0 : content.text,
|
|
176
|
+
markdown: content.markdown && {
|
|
177
|
+
content: content.markdown.content,
|
|
178
|
+
custom_template_id: content.markdown.templateId,
|
|
179
|
+
template_id: content.markdown.legacyTemplateId,
|
|
180
|
+
params: content.markdown.params,
|
|
181
|
+
force_verify_image_resource: content.markdown.verifyImages
|
|
182
|
+
},
|
|
183
|
+
media: content.media && { file_info: content.media.fileId },
|
|
184
|
+
ark: content.card && {
|
|
185
|
+
template_id: content.card.templateId,
|
|
186
|
+
kv: content.card.fields.map((field) => ({
|
|
187
|
+
key: field.key,
|
|
188
|
+
value: field.value,
|
|
189
|
+
obj: field.objects?.map((object) => ({ obj_kv: object.fields }))
|
|
190
|
+
}))
|
|
191
|
+
},
|
|
192
|
+
keyboard: content.keyboard && {
|
|
193
|
+
id: content.keyboard.templateId,
|
|
194
|
+
content: content.keyboard.rows && { rows: content.keyboard.rows.map((buttons) => ({ buttons: buttons.map((button) => ({
|
|
195
|
+
id: button.id,
|
|
196
|
+
group_id: button.groupId,
|
|
197
|
+
render_data: {
|
|
198
|
+
label: button.label,
|
|
199
|
+
visited_label: button.visitedLabel ?? button.label,
|
|
200
|
+
style: button.style
|
|
201
|
+
},
|
|
202
|
+
action: {
|
|
203
|
+
type: {
|
|
204
|
+
link: 0,
|
|
205
|
+
callback: 1,
|
|
206
|
+
command: 2
|
|
207
|
+
}[button.action.type],
|
|
208
|
+
data: button.action.data,
|
|
209
|
+
permission: {
|
|
210
|
+
type: {
|
|
211
|
+
users: 0,
|
|
212
|
+
admins: 1,
|
|
213
|
+
everyone: 2,
|
|
214
|
+
roles: 3
|
|
215
|
+
}[button.action.permission?.type ?? "everyone"],
|
|
216
|
+
specify_user_ids: button.action.permission?.userIds,
|
|
217
|
+
specify_role_ids: button.action.permission?.roleIds
|
|
218
|
+
},
|
|
219
|
+
enter: button.action.autoSend,
|
|
220
|
+
reply: button.action.reply,
|
|
221
|
+
unsupport_tips: button.action.hint,
|
|
222
|
+
anchor: button.action.anchor,
|
|
223
|
+
click_limit: button.action.clickLimit,
|
|
224
|
+
modal: button.action.modal && {
|
|
225
|
+
content: button.action.modal.content,
|
|
226
|
+
confirm_text: button.action.modal.confirmText,
|
|
227
|
+
cancel_text: button.action.modal.cancelText
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
})) })) }
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
/** Translate semantic framework actions. Undefined means a legacy/channel action. */
|
|
235
|
+
const handleFrameworkAction = async (client, data, botId) => {
|
|
236
|
+
const payload = data.payload ?? {};
|
|
237
|
+
const p = payload.params ?? {};
|
|
238
|
+
const event = payload.event ?? {};
|
|
239
|
+
const target = p.target ?? payload.target ?? event.Target ?? (event.SpaceId?.startsWith("GROUP:") ? {
|
|
240
|
+
scope: "group",
|
|
241
|
+
targetId: event.ChannelId || event.GuildId
|
|
242
|
+
} : event.SpaceId?.startsWith("GUILD:") ? {
|
|
243
|
+
scope: "channel",
|
|
244
|
+
targetId: event.ChannelId || event.SpaceId.slice(6)
|
|
245
|
+
} : event.IsPrivate && event.UserId ? {
|
|
246
|
+
scope: "c2c",
|
|
247
|
+
targetId: event.UserId
|
|
248
|
+
} : void 0);
|
|
249
|
+
const groupId = p.guildId ?? payload.GuildId ?? (target?.scope === "group" ? target.targetId : void 0) ?? event.GuildId ?? event.ChannelId;
|
|
250
|
+
const userId = p.userId ?? payload.UserId ?? event.UserId;
|
|
251
|
+
const need = (value, name) => {
|
|
252
|
+
if (!value) throw new Error(`Missing ${name}`);
|
|
253
|
+
return value;
|
|
254
|
+
};
|
|
255
|
+
const requireGroup = () => {
|
|
256
|
+
if (target?.scope !== "group") throw new Error("This operation requires an explicit group target");
|
|
257
|
+
return need(groupId, "guildId");
|
|
258
|
+
};
|
|
259
|
+
const destination = () => {
|
|
260
|
+
if (!target || !["group", "c2c"].includes(target.scope) || !target.targetId) throw new Error("A group or c2c target is required");
|
|
261
|
+
return target;
|
|
262
|
+
};
|
|
263
|
+
const delivery = () => {
|
|
264
|
+
if (p.wakeup && (p.replyId || p.eventId)) throw new Error("wakeup cannot be combined with replyId/eventId");
|
|
265
|
+
if (p.replyId && p.eventId) throw new Error("replyId and eventId are mutually exclusive");
|
|
266
|
+
if (p.wakeup && target?.scope === "group") throw new Error("QQ wakeup messages require a c2c target");
|
|
267
|
+
const origin = event.Target ?? (event.SpaceId?.startsWith("GROUP:") ? {
|
|
268
|
+
scope: "group",
|
|
269
|
+
targetId: event.SpaceId.slice(6)
|
|
270
|
+
} : event.IsPrivate && event.UserId ? {
|
|
271
|
+
scope: "c2c",
|
|
272
|
+
targetId: event.UserId
|
|
273
|
+
} : void 0);
|
|
274
|
+
const sameConversation = (!(origin?.BotId ?? event.BotId) || (origin?.BotId ?? event.BotId) === botId) && (!origin || !target || origin.scope === target.scope && origin.targetId === target.targetId);
|
|
275
|
+
const eventReply = sameConversation && /^(INTERACTION_CREATE|GROUP_ADD_ROBOT|GROUP_MSG_RECEIVE|C2C_MSG_RECEIVE|FRIEND_ADD)/.test(event._tag ?? "");
|
|
276
|
+
const messageReply = sameConversation && /^(GROUP_AT_MESSAGE_CREATE|GROUP_MESSAGE_CREATE|C2C_MESSAGE_CREATE)$/.test(event._tag ?? "");
|
|
277
|
+
return {
|
|
278
|
+
...p.wakeup ? { is_wakeup: true } : p.eventId ? { event_id: p.eventId } : p.replyId ? { msg_id: p.replyId } : eventReply ? { event_id: event.EventId ?? event.MessageId } : messageReply ? { msg_id: event.MessageId } : {},
|
|
279
|
+
...p.sequence !== void 0 && { msg_seq: p.sequence },
|
|
280
|
+
...p.referenceId && { message_reference: { message_id: p.referenceId } }
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
const actions = {
|
|
284
|
+
"menu.get": async () => {
|
|
285
|
+
const r = await client.menuGet();
|
|
286
|
+
return {
|
|
287
|
+
version: r.version,
|
|
288
|
+
menu: r.menu && { items: (r.menu.items ?? []).map(fromMenuItem) }
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
"menu.set": () => {
|
|
292
|
+
if (!Array.isArray(p.menu?.items) || p.menu.items.length > 10) throw new Error("A QQ menu supports at most 10 items");
|
|
293
|
+
return client.menuPut({ menu: { items: p.menu.items.map(menuItem) } });
|
|
294
|
+
},
|
|
295
|
+
"panel.list": async () => {
|
|
296
|
+
if (!["group", "c2c"].includes(p.scope)) throw new Error("QQ panel scope must be group or c2c");
|
|
297
|
+
const r = await client.panelsList({
|
|
298
|
+
scope: p.scope,
|
|
299
|
+
...pagination(p.pagination)
|
|
300
|
+
});
|
|
301
|
+
return page(r.records.map(fromPanel), r);
|
|
302
|
+
},
|
|
303
|
+
"panel.create": async () => {
|
|
304
|
+
validatePanel(p.panel);
|
|
305
|
+
validatePanelAudience(p.scope, p.audience, p.userIds, p.guildIds);
|
|
306
|
+
return { id: (await client.panelsCreate({
|
|
307
|
+
scope: p.scope,
|
|
308
|
+
target_type: p.audience,
|
|
309
|
+
user_openids: p.userIds,
|
|
310
|
+
group_openids: p.guildIds,
|
|
311
|
+
panel: panelBody(p.panel)
|
|
312
|
+
})).panel_id };
|
|
313
|
+
},
|
|
314
|
+
"panel.get": async () => fromPanel(await client.panelsGet(need(p.id, "id"))),
|
|
315
|
+
"panel.update": () => {
|
|
316
|
+
validatePanel(p.panel);
|
|
317
|
+
return client.panelsPut(need(p.id, "id"), { panel: panelBody(p.panel) });
|
|
318
|
+
},
|
|
319
|
+
"panel.delete": () => client.panelsDelete(need(p.id, "id")),
|
|
320
|
+
"panel.targets.update": () => {
|
|
321
|
+
if (!p.userIds?.length && !p.guildIds?.length) throw new Error("Update at least one panel audience");
|
|
322
|
+
if (p.userIds) nonEmptyIds(p.userIds, 20, "user IDs");
|
|
323
|
+
if (p.guildIds) nonEmptyIds(p.guildIds, 20, "group IDs");
|
|
324
|
+
return client.panelsTargetPut(need(p.id, "id"), {
|
|
325
|
+
op: operation(p.operation),
|
|
326
|
+
user_openids: p.userIds,
|
|
327
|
+
group_openids: p.guildIds
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
"me.share": async () => (await client.generateUrlLink({ callback_data: p.data })).data,
|
|
331
|
+
"connection.gateway": () => client.gateway(),
|
|
332
|
+
"guild.bot.info": async () => {
|
|
333
|
+
const r = await client.groupsBotState(requireGroup());
|
|
334
|
+
return {
|
|
335
|
+
userId: r.member_openid,
|
|
336
|
+
joinedAt: r.joined_at,
|
|
337
|
+
role: r.member_role,
|
|
338
|
+
allowsProactiveMessages: r.allow_proactive_msg,
|
|
339
|
+
receiveMode: r.recv_msg_setting
|
|
340
|
+
};
|
|
341
|
+
},
|
|
342
|
+
"guild.mute.get": async () => {
|
|
343
|
+
const r = await client.groupsRestrictChatSetting(requireGroup());
|
|
344
|
+
const rule = r.global_rule;
|
|
345
|
+
return {
|
|
346
|
+
global: rule && {
|
|
347
|
+
mode: rule.mode,
|
|
348
|
+
schedules: rule.schedule_rules?.map((s) => ({
|
|
349
|
+
id: s.task_id,
|
|
350
|
+
start: s.start_at,
|
|
351
|
+
end: s.end_at,
|
|
352
|
+
enabled: s.enabled
|
|
353
|
+
})),
|
|
354
|
+
recurring: rule.recurring_rules?.map((s) => ({
|
|
355
|
+
id: s.task_id,
|
|
356
|
+
weekdays: s.weekdays,
|
|
357
|
+
start: s.start_time,
|
|
358
|
+
end: s.end_time,
|
|
359
|
+
enabled: s.enabled
|
|
360
|
+
}))
|
|
361
|
+
},
|
|
362
|
+
members: (r.members ?? []).map((m) => ({
|
|
363
|
+
userId: m.member_openid,
|
|
364
|
+
userName: m.username,
|
|
365
|
+
unionId: m.union_openid,
|
|
366
|
+
expiresAt: m.mute_expire_at
|
|
367
|
+
}))
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
"member.kick.batch": async () => {
|
|
371
|
+
const r = await client.groupsBatchRemoveMembers(requireGroup(), {
|
|
372
|
+
member_openids: memberIds(p.userIds),
|
|
373
|
+
add_to_member_blacklist: p.blacklist
|
|
374
|
+
});
|
|
375
|
+
return removal(r);
|
|
376
|
+
},
|
|
377
|
+
"member.blacklist.list": async () => {
|
|
378
|
+
const r = await client.groupsMemberBlacklist(requireGroup(), pagination(p.pagination));
|
|
379
|
+
return page(r.users.map((m) => ({
|
|
380
|
+
userId: m.member_openid,
|
|
381
|
+
userName: m.username,
|
|
382
|
+
unionId: m.union_openid,
|
|
383
|
+
isBot: m.bot,
|
|
384
|
+
bannedAt: m.banned_at
|
|
385
|
+
})), r);
|
|
386
|
+
},
|
|
387
|
+
"member.blacklist.update": async () => {
|
|
388
|
+
const r = await client.groupsMemberBlacklistPost(requireGroup(), {
|
|
389
|
+
op: operation(p.operation),
|
|
390
|
+
member_openids: memberIds(p.userIds)
|
|
391
|
+
});
|
|
392
|
+
return blacklistResult(r);
|
|
393
|
+
},
|
|
394
|
+
"member.mute.batch": () => client.groupsRestrictChatSettingPost(requireGroup(), { members: nonEmptyIds(p.members?.map((m) => m.userId), 20, "member IDs").map((userId, index) => {
|
|
395
|
+
const member = p.members[index];
|
|
396
|
+
return {
|
|
397
|
+
op: member.operation === "remove" ? "del" : member.operation,
|
|
398
|
+
member_openid: userId,
|
|
399
|
+
mute_expire_at: member.expiresAt
|
|
400
|
+
};
|
|
401
|
+
}) }),
|
|
402
|
+
"request.guild.list": async () => {
|
|
403
|
+
const id = requireGroup();
|
|
404
|
+
const r = await client.groupsJoinRequestList(id, pagination(p.pagination));
|
|
405
|
+
return page((r.list ?? []).map((item) => normalizeJoinRequest(item, id)), r);
|
|
406
|
+
},
|
|
407
|
+
"request.guild.decide": () => client.groupsApprovalJoinRequest(requireGroup(), need(p.userId, "userId"), {
|
|
408
|
+
op: p.approve ? "approve" : "decline",
|
|
409
|
+
join_request_id: p.requestId,
|
|
410
|
+
reject_reason: p.reason,
|
|
411
|
+
add_to_member_blacklist: p.blacklist
|
|
412
|
+
}),
|
|
413
|
+
"request.policy.list": async () => {
|
|
414
|
+
const r = await client.groupsJoinApprovalStrategies(pagination(p.pagination));
|
|
415
|
+
return page((r.strategies ?? []).map(policy), r);
|
|
416
|
+
},
|
|
417
|
+
"request.policy.create": async () => {
|
|
418
|
+
if (Boolean(p.guildIds) === Boolean(p.guildNumbers)) throw new Error("Provide exactly one of guildIds or guildNumbers");
|
|
419
|
+
const ids = p.guildIds ?? p.guildNumbers;
|
|
420
|
+
if (!Array.isArray(ids) || ids.length < 1 || ids.length > 100) throw new Error("Expected 1 to 100 groups");
|
|
421
|
+
return policy(await client.groupsJoinApprovalStrategyCreate(policyBody(p)));
|
|
422
|
+
},
|
|
423
|
+
"request.policy.update": async () => {
|
|
424
|
+
if (p.guilds && Boolean(p.guilds.ids?.length) === Boolean(p.guilds.numbers?.length)) throw new Error("Policy group updates require exactly one of ids or numbers");
|
|
425
|
+
if (p.guilds?.ids) nonEmptyIds(p.guilds.ids, 100, "group IDs");
|
|
426
|
+
if (p.guilds?.numbers) nonEmptyIds(p.guilds.numbers, 100, "group numbers");
|
|
427
|
+
const r = await client.groupsJoinApprovalStrategyPatch(need(p.id, "id"), {
|
|
428
|
+
...policyBody(p),
|
|
429
|
+
group_action: p.guilds && {
|
|
430
|
+
op: operation(p.guilds.operation),
|
|
431
|
+
group_openids: p.guilds.ids,
|
|
432
|
+
group_ids: p.guilds.numbers
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
return {
|
|
436
|
+
enabled: r.is_enable === void 0 ? void 0 : r.is_enable === "on",
|
|
437
|
+
expiresAt: r.expire_at
|
|
438
|
+
};
|
|
439
|
+
},
|
|
440
|
+
"request.policy.delete": () => client.groupsJoinApprovalStrategyDelete(need(p.id, "id")),
|
|
441
|
+
"request.policy.execute": () => client.groupsJoinApprovalStrategyExecute(need(p.id, "id")),
|
|
442
|
+
"request.policy.whitelist": async () => {
|
|
443
|
+
const r = await client.groupsJoinApprovalStrategyWhitelistUsers(need(p.id, "id"), {
|
|
444
|
+
op: operation(p.operation),
|
|
445
|
+
whitelist_users: nonEmptyIds(p.userIds, 1e4, "whitelist user IDs")
|
|
446
|
+
});
|
|
447
|
+
return {
|
|
448
|
+
id: r.strategy_id,
|
|
449
|
+
whitelistCount: r.whitelist_user_count,
|
|
450
|
+
updatedAt: r.updated_at
|
|
451
|
+
};
|
|
452
|
+
},
|
|
453
|
+
"message.typing": async () => {
|
|
454
|
+
const t = destination();
|
|
455
|
+
if (t.scope !== "c2c") throw new Error("QQ typing is only available in c2c");
|
|
456
|
+
if (p.duration !== void 0 && (!Number.isInteger(p.duration) || p.duration < 1 || p.duration > 60)) throw new Error("QQ typing duration must be an integer from 1 to 60 seconds");
|
|
457
|
+
return receipt(await client.usersOpenMessages(t.targetId, {
|
|
458
|
+
msg_type: 6,
|
|
459
|
+
input_notify: {
|
|
460
|
+
input_type: 1,
|
|
461
|
+
input_second: p.duration ?? 60
|
|
462
|
+
},
|
|
463
|
+
...delivery()
|
|
464
|
+
}));
|
|
465
|
+
},
|
|
466
|
+
"message.stream": async () => {
|
|
467
|
+
const t = destination();
|
|
468
|
+
if (t.scope !== "c2c") throw new Error("QQ streaming is only available in c2c");
|
|
469
|
+
if (!p.text || !Number.isInteger(p.index) || p.index < 0) throw new Error("QQ streaming requires non-empty text and a non-negative integer index");
|
|
470
|
+
const r = await client.streamMessages(t.targetId, {
|
|
471
|
+
...delivery(),
|
|
472
|
+
input_mode: p.mode,
|
|
473
|
+
input_state: p.state === "complete" ? 10 : 1,
|
|
474
|
+
content_type: p.contentType,
|
|
475
|
+
content_raw: p.text,
|
|
476
|
+
index: p.index,
|
|
477
|
+
stream_msg_id: p.streamId
|
|
478
|
+
});
|
|
479
|
+
return {
|
|
480
|
+
...receipt(r),
|
|
481
|
+
remainingLength: r.remain_msg_len
|
|
482
|
+
};
|
|
483
|
+
},
|
|
484
|
+
"media.prepare": async () => {
|
|
485
|
+
const t = destination();
|
|
486
|
+
const body = {
|
|
487
|
+
file_type: mediaType(p.type),
|
|
488
|
+
file_name: p.name,
|
|
489
|
+
file_size: p.size,
|
|
490
|
+
md5: p.hashes.md5,
|
|
491
|
+
sha1: p.hashes.sha1,
|
|
492
|
+
md5_10m: p.hashes.headMd5
|
|
493
|
+
};
|
|
494
|
+
const r = await (t.scope === "group" ? client.groupUploadPrepare(t.targetId, body) : client.usersUploadPrepare(t.targetId, body));
|
|
495
|
+
return {
|
|
496
|
+
id: r.upload_id,
|
|
497
|
+
blockSize: r.block_size,
|
|
498
|
+
parts: r.parts.map((part) => ({
|
|
499
|
+
index: part.index,
|
|
500
|
+
url: part.presigned_url,
|
|
501
|
+
size: part.block_size
|
|
502
|
+
})),
|
|
503
|
+
config: {
|
|
504
|
+
concurrency: r.upload_config.concurrency,
|
|
505
|
+
retryTimeout: r.upload_config.retry_timeout,
|
|
506
|
+
retryDelay: r.upload_config.retry_delay
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
},
|
|
510
|
+
"media.part.finish": () => {
|
|
511
|
+
const t = destination();
|
|
512
|
+
const body = {
|
|
513
|
+
upload_id: p.uploadId,
|
|
514
|
+
part_index: p.index,
|
|
515
|
+
block_size: p.size,
|
|
516
|
+
md5: p.md5
|
|
517
|
+
};
|
|
518
|
+
return t.scope === "group" ? client.groupUploadPartFinish(t.targetId, body) : client.usersUploadPartFinish(t.targetId, body);
|
|
519
|
+
},
|
|
520
|
+
"media.complete": async () => {
|
|
521
|
+
const t = destination();
|
|
522
|
+
const body = {
|
|
523
|
+
upload_id: p.uploadId,
|
|
524
|
+
file_type: mediaType(p.type),
|
|
525
|
+
file_name: p.name,
|
|
526
|
+
srv_send_msg: p.send ?? false
|
|
527
|
+
};
|
|
528
|
+
return mediaReceipt(await (t.scope === "group" ? client.postRichMediaByGroup(t.targetId, body) : client.postRichMediaByUser(t.targetId, body)));
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
if (target?.scope === "group") Object.assign(actions, {
|
|
532
|
+
"guild.info": async () => {
|
|
533
|
+
const r = await client.groupsInfo(requireGroup());
|
|
534
|
+
return {
|
|
535
|
+
GuildId: r.group_openid,
|
|
536
|
+
GuildName: r.group_name,
|
|
537
|
+
Description: r.group_finger_memo,
|
|
538
|
+
Category: r.group_class_text,
|
|
539
|
+
Tags: r.group_tags,
|
|
540
|
+
MemberCount: r.group_member_num
|
|
541
|
+
};
|
|
542
|
+
},
|
|
543
|
+
"member.info": async () => member(await client.groupsMembersMessage(requireGroup(), need(userId, "userId")), requireGroup()),
|
|
544
|
+
"member.list": async () => {
|
|
545
|
+
const id = requireGroup();
|
|
546
|
+
const r = await client.groupsMembers(id, { cursor: pagination(p).cursor });
|
|
547
|
+
return page(r.members.map((item) => member(item, id)), r);
|
|
548
|
+
},
|
|
549
|
+
"member.kick": async () => removal(await client.groupsBatchRemoveMembers(requireGroup(), { member_openids: [need(userId, "userId")] })),
|
|
550
|
+
"member.ban": async () => removal(await client.groupsBatchRemoveMembers(requireGroup(), {
|
|
551
|
+
member_openids: [need(userId, "userId")],
|
|
552
|
+
add_to_member_blacklist: true
|
|
553
|
+
})),
|
|
554
|
+
"member.unban": async () => blacklistResult(await client.groupsMemberBlacklistPost(requireGroup(), {
|
|
555
|
+
op: "del",
|
|
556
|
+
member_openids: [need(userId, "userId")]
|
|
557
|
+
})),
|
|
558
|
+
"member.mute": () => client.groupsRestrictChatSettingPost(requireGroup(), { members: [{
|
|
559
|
+
member_openid: need(userId, "userId"),
|
|
560
|
+
op: p.duration === 0 ? "del" : "add",
|
|
561
|
+
mute_expire_at: p.duration === 0 ? "" : new Date(Date.now() + p.duration * 1e3).toISOString()
|
|
562
|
+
}] }),
|
|
563
|
+
"guild.mute": () => {
|
|
564
|
+
throw new Error("QQ group API supports member muting, not setting global mute rules");
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
if (data.action === "message.send" && p.content) actions["message.send"] = async () => {
|
|
568
|
+
const t = destination();
|
|
569
|
+
const body = {
|
|
570
|
+
...toQQMessage(p.content),
|
|
571
|
+
...delivery()
|
|
572
|
+
};
|
|
573
|
+
return receipt(await (t.scope === "group" ? client.groupOpenMessages(t.targetId, body) : client.usersOpenMessages(t.targetId, body)));
|
|
574
|
+
};
|
|
575
|
+
if (data.action === "message.delete" && target && ["group", "c2c"].includes(target.scope)) actions["message.delete"] = () => target.scope === "group" ? client.groupMessageDelete(target.targetId, need(payload.MessageId, "MessageId")) : client.userMessageDelete(target.targetId, need(payload.MessageId, "MessageId"));
|
|
576
|
+
if (data.action === "message.send" && p.format && target && ["group", "c2c"].includes(target.scope)) {
|
|
577
|
+
const requestedBot = target.BotId ?? payload.BotId ?? event.BotId;
|
|
578
|
+
if (requestedBot && requestedBot !== botId) return [createResult(ResultCode.FailParams, "BotId does not match this adapter", null)];
|
|
579
|
+
try {
|
|
580
|
+
const d = delivery();
|
|
581
|
+
const options = {
|
|
582
|
+
...p,
|
|
583
|
+
replyId: d.msg_id,
|
|
584
|
+
eventId: d.event_id,
|
|
585
|
+
wakeup: d.is_wakeup
|
|
586
|
+
};
|
|
587
|
+
return target.scope === "group" ? await GROUP_AT_MESSAGE_CREATE(client, { ChannelId: target.targetId }, p.format, options) : await C2C_MESSAGE_CREATE(client, { UserId: target.targetId }, p.format, options);
|
|
588
|
+
} catch (error) {
|
|
589
|
+
return [createResult(ResultCode.Fail, data.action, error?.response?.data ?? error?.message ?? error)];
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const action = actions[data.action];
|
|
593
|
+
if (!action) {
|
|
594
|
+
const guildOperation = /^(guild|member|role)\./.test(data.action) || data.action === "me.guilds";
|
|
595
|
+
const hasSourceEvent = Boolean(payload.event && Object.keys(payload.event).length > 0);
|
|
596
|
+
if (guildOperation && (target?.scope === "group" || target?.scope === "c2c" || !target && hasSourceEvent)) return [createResult(target ? ResultCode.Fail : ResultCode.FailParams, target ? `${data.action} is not supported for QQ ${target.scope}` : `${data.action} requires an explicit target scope`, null)];
|
|
597
|
+
if ((/^(permission|reaction|schedule|forum|audio)\./.test(data.action) || /^channel\.(info|update|delete|legacy-announcement\.|api-permission\.request)/.test(data.action) || /^message\.(get|edit|pin|unpin)$/.test(data.action)) && target?.scope !== "channel" && target?.scope !== "direct") return [createResult(target ? ResultCode.Fail : ResultCode.FailParams, target ? `${data.action} is not supported for QQ ${target.scope}` : `${data.action} requires an explicit target scope`, null)];
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if ((target?.BotId ?? payload.BotId ?? event.BotId) && (target?.BotId ?? payload.BotId ?? event.BotId) !== botId) return [createResult(ResultCode.FailParams, "BotId does not match this adapter", null)];
|
|
601
|
+
try {
|
|
602
|
+
const result = await action();
|
|
603
|
+
if ([
|
|
604
|
+
"member.kick",
|
|
605
|
+
"member.ban",
|
|
606
|
+
"member.kick.batch",
|
|
607
|
+
"member.unban",
|
|
608
|
+
"member.blacklist.update"
|
|
609
|
+
].includes(data.action)) {
|
|
610
|
+
const outcome = result;
|
|
611
|
+
if (outcome.status !== void 0 && outcome.status !== "success" || outcome.failedBlacklistIds?.length || outcome.failedUserIds?.length) return [createResult(ResultCode.Warn, `${data.action}: some operations failed; inspect result data`, result)];
|
|
612
|
+
}
|
|
613
|
+
return [createResult(ResultCode.Ok, data.action, result)];
|
|
614
|
+
} catch (error) {
|
|
615
|
+
return [createResult(ResultCode.Fail, data.action, error?.response?.data ?? error?.message ?? error)];
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
//#endregion
|
|
620
|
+
export { handleFrameworkAction, toQQMessage };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { EventsEnum, JoinRequestInfo, MessageMediaItem, User } from "alemonjs";
|
|
2
|
+
//#region src/framework-events.d.ts
|
|
3
|
+
/** Preserve scoped identities in every author/mention, not only the top-level sender. */
|
|
4
|
+
declare const normalizeMessageUser: (raw: any, scope: 'group' | 'c2c' | 'channel') => User;
|
|
5
|
+
declare const normalizeJoinRequest: (raw: any, guildId?: any) => JoinRequestInfo;
|
|
6
|
+
declare const normalizeAttachments: (attachments?: any[]) => MessageMediaItem[];
|
|
7
|
+
/** Standard fields expose platform capabilities without business code reading raw QQ data. */
|
|
8
|
+
declare const normalizeFrameworkEvent: (event: EventsEnum, acknowledged?: boolean) => EventsEnum;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { normalizeAttachments, normalizeFrameworkEvent, normalizeJoinRequest, normalizeMessageUser };
|