Skip to content

Commit b3997da

Browse files
committed
feat: 支持 pickMember、kick 群成员操作
1 parent 5236db5 commit b3997da

File tree

4 files changed

+224
-1
lines changed

4 files changed

+224
-1
lines changed

packages/napcat-sdk/src/napcat.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ export class NapCat {
223223
recall: this.recallMsg.bind(this),
224224
ban: this.setGroupBan.bind(this, group_id),
225225
sendMsg: this.sendGroupMsg.bind(this, group_id),
226+
pickMember: this.getGroupMemberInfo.bind(this, group_id),
227+
kick: this.kickGroupMember.bind(this, group_id),
226228
}
227229
}
228230

@@ -765,6 +767,13 @@ export class NapCat {
765767
return this.api<GroupMemberInfo>('get_group_member_info', { group_id, user_id })
766768
}
767769

770+
/**
771+
* 踢出群成员
772+
*/
773+
kickGroupMember(group_id: number, user_id: number, reject_add_request: boolean = false): Promise<void> {
774+
return this.api<void>('set_group_kick', { group_id, user_id, reject_add_request })
775+
}
776+
768777
/**
769778
* 机器人是否在线
770779
*/

packages/napcat-sdk/src/segment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const segment = {
1111
/** 创建一个文本消息片段 */
1212
text: (text: string): SendElement => createSegment('text', { text }),
1313
/** 创建一个艾特消息片段 */
14-
at: (qq: 'all' | (string & {})): SendElement => createSegment('at', { qq }),
14+
at: (qq: 'all' | (string & {}) | number): SendElement => createSegment('at', { qq }),
1515
/** 创建一个 QQ 表情消息片段 */
1616
face: (id: number): SendElement => createSegment('face', { id }),
1717
/** 创建一个回复消息片段 */

packages/napcat-sdk/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,10 @@ export interface Group {
581581
ban: (user_id: number, duration: number) => Promise<any>
582582
/** 发送群消息 */
583583
sendMsg: SendMsg
584+
/** 获取群成员信息 */
585+
pickMember: (user_id: number) => Promise<GroupMemberInfo>
586+
/** 踢出群成员 */
587+
kick: (user_id: number, reject_add_request?: boolean) => Promise<void>
584588
}
585589

586590
export type GroupWithInfo = Group & Awaited<ReturnType<Group['getInfo']>>

plugins/进群验证/index.ts

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { definePlugin } from 'mioki'
2+
3+
interface TempUser {
4+
triedTimes: number
5+
verifyNumbers: [number, number, number]
6+
kickTimer: ReturnType<typeof setTimeout>
7+
remindTimer: ReturnType<typeof setTimeout> | null
8+
}
9+
10+
const OPTS = ['+', '-'] as const
11+
12+
const DEFAULT_CONFIG = {
13+
timeout: 3 * 60_000,
14+
verifyMode: '精确' as '精确' | '模糊',
15+
maxRetryTimes: 3,
16+
numberRange: [10, 99] as [number, number],
17+
lastRemindTime: 60_000 as number | false,
18+
cmds: {
19+
on: '#开启验证',
20+
off: '#关闭验证',
21+
bypass: '#绕过验证',
22+
reverify: '#重新验证',
23+
},
24+
tips: {
25+
fallback: '✅ 验证成功,欢迎入群,这个号是机器人,有问题请先查看群公告',
26+
} as Record<number | 'fallback', string>,
27+
groups: [] as number[],
28+
}
29+
30+
export default definePlugin({
31+
name: '进群验证',
32+
version: '1.0.2',
33+
priority: 10,
34+
description: '进群验证',
35+
setup: async (ctx) => {
36+
const tempUsers = new Map<string, TempUser>()
37+
38+
const config = await ctx.createStore(DEFAULT_CONFIG, { __dirname })
39+
40+
ctx.handle('message.group', async (e) => {
41+
const text = ctx.text(e)
42+
43+
const isMatchCmd = Object.values(config.data.cmds).includes(text)
44+
if (!isMatchCmd) return
45+
46+
const mentionedUser = await ctx.getMentionedUserId(e)
47+
48+
if (!ctx.hasRight(e)) return e.reply('不支持小男娘使用喵~')
49+
50+
switch (text) {
51+
case config.data.cmds.on: {
52+
const info = await e.group.getMemberInfo(ctx.bot.uin)
53+
54+
if (info.role === 'member') {
55+
return e.reply('权限不足喵,请给我群主/管理员喵~')
56+
}
57+
58+
config.update((c) => void (c.groups = ctx.unique([...c.groups, e.group_id])))
59+
return e.reply('✅ 已开启进群验证喵~')
60+
}
61+
case config.data.cmds.off: {
62+
config.update((c) => {
63+
const idx = c.groups.indexOf(e.group_id)
64+
if (idx === -1) return e.reply('进群验证已关闭喵~')
65+
c.groups.splice(idx, 1)
66+
return e.reply('✅ 已关闭进群验证喵~')
67+
})
68+
}
69+
}
70+
71+
if (!mentionedUser) return e.reply('请 @ 需要操作的用户喵~')
72+
if (!config.data.groups.includes(e.group.group_id)) return e.reply('进群验证未开启喵~')
73+
74+
switch (text) {
75+
case config.data.cmds.bypass: {
76+
clearUser(e.group.group_id, mentionedUser)
77+
return e.reply(`✅ 验证成功,欢迎入群喵~`)
78+
}
79+
case config.data.cmds.reverify: {
80+
if (ctx.bot.uin === mentionedUser) return e.reply('八嘎!!!')
81+
if (ctx.hasRight(mentionedUser)) return e.reply('不能对我的主人这么无礼喵~')
82+
83+
// if (!(await ctx.canBan(e.group.group_id, mentionedUser))) {
84+
// return e.reply('权限不足喵,请给我群主喵~')
85+
// }
86+
87+
return startVerifyUser(e.group.group_id, mentionedUser)
88+
}
89+
}
90+
})
91+
92+
ctx.handle('notice.group.increase', async (e) => {
93+
const { group_id, user_id, group } = e
94+
const member = await group.getMemberInfo(user_id)
95+
96+
if (!config.data.groups.includes(group_id)) return
97+
if (ctx.hasRight(e) || member.role !== 'member') return
98+
99+
startVerifyUser(group_id, user_id)
100+
})
101+
102+
ctx.handle('notice.group.decrease', async (e) => {
103+
const { group_id, user_id } = e
104+
if (!config.data.groups.includes(group_id)) return
105+
106+
if (tempUsers.has(key(group_id, user_id))) {
107+
clearUser(group_id, user_id)
108+
await ctx.bot.sendGroupMsg(group_id, `${user_id} 溜掉了,验证流程结束了喵`)
109+
}
110+
})
111+
112+
ctx.handle('message.group.normal', async (e) => {
113+
const { group_id, sender, group } = e
114+
const member = await group.pickMember(sender.user_id)
115+
const { tips, groups, maxRetryTimes } = config.data
116+
117+
if (ctx.hasRight(e) || member.role !== 'member') return
118+
if (!groups.includes(group_id)) return
119+
120+
const user = tempUsers.get(key(group_id, sender.user_id))
121+
if (!user) return
122+
123+
const [_, __, result] = user.verifyNumbers
124+
const msg = ctx.text(e)
125+
126+
if (msg === String(result)) {
127+
const tip = tips[group_id] || tips.fallback
128+
ctx.bot.sendGroupMsg(group_id, tip)
129+
clearUser(group_id, sender.user_id)
130+
} else {
131+
user.triedTimes += 1
132+
if (user.triedTimes >= maxRetryTimes) {
133+
clearUser(group_id, sender.user_id)
134+
await e.reply([ctx.segment.at(sender.user_id), ` ❌ 验证失败,次数达上限了喵,请重新申请喵`])
135+
await e.group.kick(sender.user_id)
136+
} else {
137+
await ctx.bot.sendGroupMsg(group_id, [
138+
ctx.segment.at(sender.user_id),
139+
` ❌ 回答错误,还剩 ${maxRetryTimes - user.triedTimes} 次机会喵`,
140+
])
141+
}
142+
}
143+
})
144+
145+
function startVerifyUser(group_id: number, user_id: number) {
146+
const user = tempUsers.get(key(group_id, user_id))
147+
if (user) clearUser(group_id, user_id)
148+
149+
const { lastRemindTime, timeout, numberRange: range } = config.data
150+
const [x, y] = [ctx.randomInt(range[0], range[1]), ctx.randomInt(range[0], range[1])]
151+
const [m, n] = [Math.max(x, y), Math.min(x, y)]
152+
const operator = ctx.randomItem(OPTS)
153+
const isPlus = operator === '+'
154+
const verifyCode = isPlus ? m + n : m - n
155+
156+
const kickTimer = setTimeout(async () => {
157+
clearUser(group_id, user_id)
158+
await ctx.bot.sendGroupMsg(group_id, [ctx.segment.at(user_id), `❌ 验证超时,请重新申请喵`])
159+
await (await ctx.bot.pickGroup(group_id))?.kick(user_id)
160+
}, timeout)
161+
162+
const remindTimer =
163+
lastRemindTime && lastRemindTime > 0
164+
? setTimeout(() => {
165+
ctx.bot.sendGroupMsg(group_id, [
166+
ctx.segment.at(user_id),
167+
` 进群验证还剩 ${lastRemindTime / 1000} 秒,请发送「${mathFormula}」的运算结果,不听话会被移出群聊喵`,
168+
])
169+
}, timeout - lastRemindTime)
170+
: null
171+
172+
tempUsers.set(key(group_id, user_id), {
173+
triedTimes: 0,
174+
verifyNumbers: [m, n, verifyCode],
175+
kickTimer,
176+
remindTimer,
177+
})
178+
179+
const seconds = Math.round(timeout / 1000)
180+
const mathFormula = `${m}${operator}${n}`
181+
182+
ctx.bot.sendGroupMsg(group_id, [
183+
ctx.segment.at(user_id),
184+
` 请在「${seconds}」秒内发送「${mathFormula}」的运算结果,不听话会被移出群聊喵`,
185+
])
186+
}
187+
188+
function clearUser(group_id: number, user_id: number) {
189+
const mapKey = key(group_id, user_id)
190+
const user = tempUsers.get(mapKey)
191+
192+
if (user) {
193+
user.kickTimer && clearTimeout(user.kickTimer)
194+
user.remindTimer && clearTimeout(user.remindTimer)
195+
tempUsers.delete(mapKey)
196+
}
197+
}
198+
199+
function key(group_id: number, user_id: number) {
200+
return `${group_id}_${user_id}`
201+
}
202+
203+
return () => {
204+
for (const [key] of tempUsers) {
205+
const [group_id, user_id] = key.split('_').map(Number)
206+
clearUser(group_id, user_id)
207+
}
208+
}
209+
},
210+
})

0 commit comments

Comments
 (0)