koishi-plugin-temporaryban 1.0.0
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 +80 -0
- package/lib/index.d.ts +42 -0
- package/lib/index.js +331 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# koishi-plugin-temporaryban
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/koishi-plugin-temporaryban)
|
|
4
|
+
|
|
5
|
+
一个强大的 Koishi 违禁词检测与自动禁言插件。支持多种检测方式,并提供详细的邮件通知功能。
|
|
6
|
+
|
|
7
|
+
## ✨ 特性
|
|
8
|
+
|
|
9
|
+
- **多重检测机制**:
|
|
10
|
+
- 🏠 **本地词库**:支持自定义正则表达式或简单词库匹配。
|
|
11
|
+
- 🌐 **在线 API**:集成 ApiHz 敏感词检测接口,支持智能识别。
|
|
12
|
+
- **灵活的惩罚机制**:
|
|
13
|
+
- 自动撤回违规消息。
|
|
14
|
+
- 累计违规次数触发禁言。
|
|
15
|
+
- 可配置的检测时间窗口和禁言时长。
|
|
16
|
+
- **邮件通知**:
|
|
17
|
+
- 违规触发时自动发送邮件给管理员。
|
|
18
|
+
- 包含详细的违规信息(用户、群组、触发词)。
|
|
19
|
+
- **白名单支持**:可为特定用户设置白名单,免受检测。
|
|
20
|
+
|
|
21
|
+
## 📦 安装
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install koishi-plugin-temporaryban
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## ⚙️ 配置项
|
|
28
|
+
|
|
29
|
+
### 全局设置
|
|
30
|
+
|
|
31
|
+
#### 邮件通知 (SMTP)
|
|
32
|
+
|
|
33
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
34
|
+
| --- | --- | --- | --- |
|
|
35
|
+
| `smtp.host` | string | `smtp.example.com` | SMTP 服务器地址 |
|
|
36
|
+
| `smtp.port` | number | `465` | SMTP 端口 (通常 SSL 为 465) |
|
|
37
|
+
| `smtp.secure` | boolean | `true` | 是否使用 SSL/TLS |
|
|
38
|
+
| `smtp.user` | string | `user@example.com` | 发件人邮箱账号 |
|
|
39
|
+
| `smtp.pass` | string | `password` | 发件人邮箱密码/授权码 |
|
|
40
|
+
| `smtp.senderName` | string | `Koishi Bot` | 邮件发件人名称 |
|
|
41
|
+
| `smtp.senderEmail` | string | `bot@example.com` | 邮件发件人地址 |
|
|
42
|
+
| `smtp.receivers` | string[] | `[]` | 接收通知的管理员邮箱列表 |
|
|
43
|
+
|
|
44
|
+
#### 在线 API (ApiHz)
|
|
45
|
+
|
|
46
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
47
|
+
| --- | --- | --- | --- |
|
|
48
|
+
| `api.apiUrl` | string | `https://cn.apihz.cn/api/zici/mgc.php` | 接口地址 |
|
|
49
|
+
| `api.apiId` | string | - | ApiHz 开发者 ID |
|
|
50
|
+
| `api.apiKey` | string | - | ApiHz 开发者 Key |
|
|
51
|
+
|
|
52
|
+
### 群组设置 (Groups)
|
|
53
|
+
|
|
54
|
+
您可以为每个群组单独配置检测规则。
|
|
55
|
+
|
|
56
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
57
|
+
| --- | --- | --- | --- |
|
|
58
|
+
| `groupId` | string | - | **必填**,需要监控的群号 |
|
|
59
|
+
| `enable` | boolean | `true` | 是否启用监控 |
|
|
60
|
+
| `detectionMethod` | `local` \| `api` | `local` | 检测方式:本地词库或在线 API |
|
|
61
|
+
| `localBadWordDict` | string | - | 本地违禁词库,格式:`(1.词1)(2.词2)` |
|
|
62
|
+
| `whitelist` | object[] | `[]` | 白名单用户列表,包含 `userId` |
|
|
63
|
+
| `triggerThreshold` | number | `3` | 触发禁言的累计违规次数 |
|
|
64
|
+
| `triggerWindowMinutes` | number | `5` | 违规计数的时间窗口(分钟) |
|
|
65
|
+
| `muteMinutes` | number | `10` | 禁言时长(分钟) |
|
|
66
|
+
|
|
67
|
+
## 💡 使用示例
|
|
68
|
+
|
|
69
|
+
1. **配置本地检测**:
|
|
70
|
+
在控制台中添加一个群组配置,选择 `detectionMethod` 为 `local`,并在 `localBadWordDict` 中填入 `(1.笨蛋)(2.傻瓜)`。
|
|
71
|
+
|
|
72
|
+
2. **配置 API 检测**:
|
|
73
|
+
在全局配置中填入 ApiHz 的 ID 和 Key,然后在群组配置中选择 `detectionMethod` 为 `api`。
|
|
74
|
+
|
|
75
|
+
3. **启用邮件通知**:
|
|
76
|
+
配置好 SMTP 信息,并添加管理员邮箱到 `receivers` 列表。一旦有用户触发违禁词,您将收到包含详细信息的邮件。
|
|
77
|
+
|
|
78
|
+
## 📝 License
|
|
79
|
+
|
|
80
|
+
MIT
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
export declare const name = "koishi-plugin-temporaryban";
|
|
3
|
+
export interface BadWordItem {
|
|
4
|
+
code: string;
|
|
5
|
+
word: string;
|
|
6
|
+
}
|
|
7
|
+
export interface WhitelistItem {
|
|
8
|
+
userId: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SmtpConfig {
|
|
11
|
+
host: string;
|
|
12
|
+
port: number;
|
|
13
|
+
secure: boolean;
|
|
14
|
+
user: string;
|
|
15
|
+
pass: string;
|
|
16
|
+
senderName: string;
|
|
17
|
+
senderEmail: string;
|
|
18
|
+
receivers: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface ApiConfig {
|
|
21
|
+
apiUrl: string;
|
|
22
|
+
apiId: string;
|
|
23
|
+
apiKey: string;
|
|
24
|
+
}
|
|
25
|
+
export interface GroupConfig {
|
|
26
|
+
id: string;
|
|
27
|
+
groupId: string;
|
|
28
|
+
enable: boolean;
|
|
29
|
+
detectionMethod: 'local' | 'api';
|
|
30
|
+
localBadWordDict: string;
|
|
31
|
+
whitelist: WhitelistItem[];
|
|
32
|
+
triggerThreshold: number;
|
|
33
|
+
triggerWindowMinutes: number;
|
|
34
|
+
muteMinutes: number;
|
|
35
|
+
}
|
|
36
|
+
export interface Config {
|
|
37
|
+
smtp: SmtpConfig;
|
|
38
|
+
api: ApiConfig;
|
|
39
|
+
groups: GroupConfig[];
|
|
40
|
+
}
|
|
41
|
+
export declare const Config: Schema<Config>;
|
|
42
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Config = exports.name = void 0;
|
|
37
|
+
exports.apply = apply;
|
|
38
|
+
const koishi_1 = require("koishi");
|
|
39
|
+
const nodemailer = __importStar(require("nodemailer"));
|
|
40
|
+
exports.name = 'koishi-plugin-temporaryban';
|
|
41
|
+
// --- Logger ---
|
|
42
|
+
const logger = new koishi_1.Logger('temporaryban');
|
|
43
|
+
// --- Schema Definition ---
|
|
44
|
+
exports.Config = koishi_1.Schema.object({
|
|
45
|
+
smtp: koishi_1.Schema.object({
|
|
46
|
+
host: koishi_1.Schema.string().description('SMTP Host').default('smtp.example.com'),
|
|
47
|
+
port: koishi_1.Schema.number().description('SMTP Port').default(465),
|
|
48
|
+
secure: koishi_1.Schema.boolean().description('Use SSL/TLS').default(true),
|
|
49
|
+
user: koishi_1.Schema.string().description('SMTP User').default('user@example.com'),
|
|
50
|
+
pass: koishi_1.Schema.string().role('secret').description('SMTP Password').default('password'),
|
|
51
|
+
senderName: koishi_1.Schema.string().description('Sender Name').default('Koishi Bot'),
|
|
52
|
+
senderEmail: koishi_1.Schema.string().description('Sender Email').default('bot@example.com'),
|
|
53
|
+
receivers: koishi_1.Schema.array(String).description('Receiver Emails').role('table'),
|
|
54
|
+
}).description('Email Notification Settings (SMTP)'),
|
|
55
|
+
api: koishi_1.Schema.object({
|
|
56
|
+
apiUrl: koishi_1.Schema.string().description('API URL').default('https://cn.apihz.cn/api/zici/mgc.php'),
|
|
57
|
+
apiId: koishi_1.Schema.string().description('Developer ID').default(''),
|
|
58
|
+
apiKey: koishi_1.Schema.string().role('secret').description('API Key').default(''),
|
|
59
|
+
}).description('API Detection Settings (ApiHz)'),
|
|
60
|
+
groups: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
61
|
+
id: koishi_1.Schema.string().hidden().default(''),
|
|
62
|
+
groupId: koishi_1.Schema.string().description('Group ID').required(),
|
|
63
|
+
enable: koishi_1.Schema.boolean().description('Enable Monitoring').default(true),
|
|
64
|
+
detectionMethod: koishi_1.Schema.union([
|
|
65
|
+
koishi_1.Schema.const('local').description('Local Dictionary'),
|
|
66
|
+
koishi_1.Schema.const('api').description('Online API (ApiHz)'),
|
|
67
|
+
]).description('Detection Method').default('local'),
|
|
68
|
+
localBadWordDict: koishi_1.Schema.string()
|
|
69
|
+
.description('Local Dictionary (Format: (1.word1)(2.word2))')
|
|
70
|
+
.default(''),
|
|
71
|
+
whitelist: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
72
|
+
userId: koishi_1.Schema.string().description('User ID')
|
|
73
|
+
})).description('Whitelist Users').role('table'),
|
|
74
|
+
triggerThreshold: koishi_1.Schema.number().description('Trigger Threshold (Count)').default(3).min(1),
|
|
75
|
+
triggerWindowMinutes: koishi_1.Schema.number().description('Trigger Window (Minutes)').default(5).min(0.1),
|
|
76
|
+
muteMinutes: koishi_1.Schema.number().description('Mute Duration (Minutes)').default(10).min(0.1),
|
|
77
|
+
}).description('Group Configuration')).description('Monitored Groups').role('list').default([])
|
|
78
|
+
}).description('Temporary Ban Plugin Configuration');
|
|
79
|
+
// --- Plugin Implementation ---
|
|
80
|
+
function apply(ctx, config) {
|
|
81
|
+
// Runtime state
|
|
82
|
+
const parsedLocalDictCache = new Map();
|
|
83
|
+
const userRecords = new Map(); // Key: groupId-userId
|
|
84
|
+
const messageThrottle = new Map();
|
|
85
|
+
const THROTTLE_LIMIT = 500; // 500ms throttle
|
|
86
|
+
// --- Helper Functions ---
|
|
87
|
+
function parseLocalDict(dictStr) {
|
|
88
|
+
const result = [];
|
|
89
|
+
if (!dictStr || !dictStr.trim())
|
|
90
|
+
return result;
|
|
91
|
+
const reg = /\(([^.]+)\.(.+?)\)/g;
|
|
92
|
+
let match;
|
|
93
|
+
while ((match = reg.exec(dictStr)) !== null) {
|
|
94
|
+
result.push({ code: match[1].trim(), word: match[2].trim() });
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
function initCache() {
|
|
99
|
+
parsedLocalDictCache.clear();
|
|
100
|
+
for (const group of config.groups) {
|
|
101
|
+
if (!group.groupId)
|
|
102
|
+
continue;
|
|
103
|
+
const parsed = parseLocalDict(group.localBadWordDict);
|
|
104
|
+
parsedLocalDictCache.set(group.groupId, parsed);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// API Detection
|
|
108
|
+
async function checkWithApi(content) {
|
|
109
|
+
if (!config.api.apiId || !config.api.apiKey) {
|
|
110
|
+
logger.warn('API ID or Key is missing. Skipping API detection.');
|
|
111
|
+
return { detected: false };
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
// Using POST as recommended
|
|
115
|
+
const response = await ctx.http.post(config.api.apiUrl, {
|
|
116
|
+
id: config.api.apiId,
|
|
117
|
+
key: config.api.apiKey,
|
|
118
|
+
words: content,
|
|
119
|
+
replacetype: 1, // Return replaced text
|
|
120
|
+
mgctype: 1, // Return detected words list
|
|
121
|
+
}, {
|
|
122
|
+
headers: {
|
|
123
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
// API returns JSON
|
|
127
|
+
// Success: { code: 200, jcstatus: 1, replacewords: "...", mgcwords: "..." }
|
|
128
|
+
// No violation: { code: 200, jcstatus: 0, ... }
|
|
129
|
+
if (response && response.code === 200) {
|
|
130
|
+
if (response.jcstatus === 1) {
|
|
131
|
+
const words = response.mgcwords ? response.mgcwords.split(',') : [];
|
|
132
|
+
return {
|
|
133
|
+
detected: true,
|
|
134
|
+
censoredText: response.replacewords,
|
|
135
|
+
detectedWords: words
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return { detected: false };
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
logger.warn(`API Error: ${JSON.stringify(response)}`);
|
|
142
|
+
return { detected: false };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
logger.error(`API Request Failed: ${err}`);
|
|
147
|
+
return { detected: false };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Local Detection
|
|
151
|
+
function checkWithLocal(content, groupId) {
|
|
152
|
+
const badWords = parsedLocalDictCache.get(groupId) || [];
|
|
153
|
+
if (badWords.length === 0)
|
|
154
|
+
return { detected: false };
|
|
155
|
+
const detectedWords = [];
|
|
156
|
+
let censoredText = content;
|
|
157
|
+
let hasViolation = false;
|
|
158
|
+
for (const item of badWords) {
|
|
159
|
+
if (content.includes(item.word)) {
|
|
160
|
+
hasViolation = true;
|
|
161
|
+
detectedWords.push(item.word);
|
|
162
|
+
// Simple replacement for local
|
|
163
|
+
censoredText = censoredText.split(item.word).join('*'.repeat(item.word.length));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (hasViolation) {
|
|
167
|
+
return {
|
|
168
|
+
detected: true,
|
|
169
|
+
censoredText,
|
|
170
|
+
detectedWords
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return { detected: false };
|
|
174
|
+
}
|
|
175
|
+
// Email Notification
|
|
176
|
+
async function sendEmail(userId, groupId, words) {
|
|
177
|
+
if (!config.smtp.host || config.smtp.receivers.length === 0)
|
|
178
|
+
return;
|
|
179
|
+
const transporter = nodemailer.createTransport({
|
|
180
|
+
host: config.smtp.host,
|
|
181
|
+
port: config.smtp.port,
|
|
182
|
+
secure: config.smtp.secure, // true for 465, false for other ports
|
|
183
|
+
auth: {
|
|
184
|
+
user: config.smtp.user,
|
|
185
|
+
pass: config.smtp.pass,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
const mailOptions = {
|
|
189
|
+
from: `"${config.smtp.senderName}" <${config.smtp.senderEmail}>`,
|
|
190
|
+
to: config.smtp.receivers.join(','),
|
|
191
|
+
subject: `[Koishi Security] Violation Detected in Group ${groupId}`,
|
|
192
|
+
text: `User ${userId} triggered forbidden words in Group ${groupId}.\n\nDetected Words: ${words.join(', ')}\n\nTime: ${new Date().toLocaleString()}`,
|
|
193
|
+
html: `
|
|
194
|
+
<h2>Violation Detected</h2>
|
|
195
|
+
<p><strong>Group ID:</strong> ${groupId}</p>
|
|
196
|
+
<p><strong>User ID:</strong> ${userId}</p>
|
|
197
|
+
<p><strong>Time:</strong> ${new Date().toLocaleString()}</p>
|
|
198
|
+
<p><strong>Detected Words:</strong></p>
|
|
199
|
+
<ul>
|
|
200
|
+
${words.map(w => `<li>${w}</li>`).join('')}
|
|
201
|
+
</ul>
|
|
202
|
+
`,
|
|
203
|
+
};
|
|
204
|
+
try {
|
|
205
|
+
const info = await transporter.sendMail(mailOptions);
|
|
206
|
+
logger.info(`Email sent: ${info.messageId}`);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
logger.error(`Failed to send email: ${err}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// --- Lifecycle ---
|
|
213
|
+
ctx.on('ready', () => {
|
|
214
|
+
initCache();
|
|
215
|
+
logger.info('Plugin initialized.');
|
|
216
|
+
});
|
|
217
|
+
ctx.setInterval(() => {
|
|
218
|
+
messageThrottle.clear();
|
|
219
|
+
}, 60 * 1000);
|
|
220
|
+
// --- Message Handler ---
|
|
221
|
+
ctx.on('message', async (session) => {
|
|
222
|
+
// Basic Filtering
|
|
223
|
+
if (session.type !== 'group' ||
|
|
224
|
+
!session.content ||
|
|
225
|
+
session.selfId === session.userId ||
|
|
226
|
+
!session.guildId)
|
|
227
|
+
return;
|
|
228
|
+
const elements = session.elements || [];
|
|
229
|
+
if (!elements.some((el) => el.type === 'text'))
|
|
230
|
+
return;
|
|
231
|
+
const groupId = session.guildId;
|
|
232
|
+
const userId = session.userId;
|
|
233
|
+
const messageId = session.messageId;
|
|
234
|
+
if (!userId)
|
|
235
|
+
return;
|
|
236
|
+
// Config Check
|
|
237
|
+
const groupConfig = config.groups.find(g => g.groupId === groupId);
|
|
238
|
+
if (!groupConfig || !groupConfig.enable)
|
|
239
|
+
return;
|
|
240
|
+
// Throttle
|
|
241
|
+
const throttleKey = `${groupId}-${userId}`;
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
if (messageThrottle.has(throttleKey) && now - messageThrottle.get(throttleKey) < THROTTLE_LIMIT) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
messageThrottle.set(throttleKey, now);
|
|
247
|
+
// Whitelist
|
|
248
|
+
if (groupConfig.whitelist.some(w => w.userId === userId))
|
|
249
|
+
return;
|
|
250
|
+
// Detection
|
|
251
|
+
let result = { detected: false };
|
|
252
|
+
if (groupConfig.detectionMethod === 'api') {
|
|
253
|
+
result = await checkWithApi(session.content);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
result = checkWithLocal(session.content, groupId);
|
|
257
|
+
}
|
|
258
|
+
if (!result.detected)
|
|
259
|
+
return;
|
|
260
|
+
// --- Violation Handling ---
|
|
261
|
+
const words = result.detectedWords || [];
|
|
262
|
+
logger.warn(`Violation: Group ${groupId} User ${userId} Words: ${words.join(', ')}`);
|
|
263
|
+
// 1. Recall Message
|
|
264
|
+
if (messageId) {
|
|
265
|
+
try {
|
|
266
|
+
if (session.bot.deleteMessage) {
|
|
267
|
+
await session.bot.deleteMessage(groupId, messageId);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
// Fallback
|
|
271
|
+
const bot = session.bot;
|
|
272
|
+
if (bot.delete_msg) {
|
|
273
|
+
await bot.delete_msg({ group_id: groupId, message_id: messageId });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
logger.error(`Recall failed: ${err}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// 2. Send Censored Text
|
|
282
|
+
if (result.censoredText) {
|
|
283
|
+
try {
|
|
284
|
+
await session.send(koishi_1.h.at(userId) + ' ' + result.censoredText);
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
logger.error(`Send message failed: ${err}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// 3. Email Notification
|
|
291
|
+
// Trigger on every violation
|
|
292
|
+
await sendEmail(userId, groupId, words);
|
|
293
|
+
// 4. Track & Punish
|
|
294
|
+
const recordKey = `${groupId}-${userId}`;
|
|
295
|
+
let record = userRecords.get(recordKey);
|
|
296
|
+
const triggerWindow = groupConfig.triggerWindowMinutes * koishi_1.Time.minute;
|
|
297
|
+
if (!record) {
|
|
298
|
+
record = { count: 1, firstTime: now };
|
|
299
|
+
userRecords.set(recordKey, record);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
if (now - record.firstTime > triggerWindow) {
|
|
303
|
+
record.count = 1;
|
|
304
|
+
record.firstTime = now;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
record.count++;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
logger.info(`Violation Count: ${record.count}/${groupConfig.triggerThreshold}`);
|
|
311
|
+
if (record.count >= groupConfig.triggerThreshold) {
|
|
312
|
+
const muteSeconds = Math.floor(groupConfig.muteMinutes * 60);
|
|
313
|
+
try {
|
|
314
|
+
if (session.bot.muteGuildMember) {
|
|
315
|
+
await session.bot.muteGuildMember(groupId, userId, muteSeconds * 1000);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
const bot = session.bot;
|
|
319
|
+
if (bot.set_group_ban) {
|
|
320
|
+
await bot.set_group_ban(groupId, userId, muteSeconds);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
logger.success(`Muted user ${userId}`);
|
|
324
|
+
userRecords.delete(recordKey);
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
logger.error(`Mute failed: ${err}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-temporaryban",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Koishi 违禁词检测与禁言插件,支持本地词库与在线API检测,违规邮件通知。",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc -w"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"koishi",
|
|
18
|
+
"plugin",
|
|
19
|
+
"ban",
|
|
20
|
+
"censorship",
|
|
21
|
+
"filter",
|
|
22
|
+
"email",
|
|
23
|
+
"notification"
|
|
24
|
+
],
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"koishi": "^4.18.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^20.0.0",
|
|
30
|
+
"koishi": "^4.18.0",
|
|
31
|
+
"typescript": "^5.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@types/nodemailer": "^7.0.9",
|
|
35
|
+
"nodemailer": "^8.0.1"
|
|
36
|
+
},
|
|
37
|
+
"koishi": {
|
|
38
|
+
"description": {
|
|
39
|
+
"en": "Forbidden words detection and temporary ban plugin for Koishi, supporting local dictionary and online API, with email notifications.",
|
|
40
|
+
"zh": "Koishi 违禁词检测与禁言插件,支持本地词库与在线API检测,违规邮件通知。"
|
|
41
|
+
},
|
|
42
|
+
"service": {
|
|
43
|
+
"required": [],
|
|
44
|
+
"optional": []
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|