mioku-plugin-impact 1.1.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/LICENSE +21 -0
- package/README.md +50 -0
- package/config.md +42 -0
- package/configs/base.ts +40 -0
- package/db.ts +229 -0
- package/handlers/dajiao.ts +50 -0
- package/handlers/open-module.ts +22 -0
- package/handlers/pk.ts +91 -0
- package/handlers/query-injection.ts +74 -0
- package/handlers/queryjj.ts +53 -0
- package/handlers/rank.ts +58 -0
- package/handlers/suo.ts +82 -0
- package/handlers/types.ts +15 -0
- package/handlers/yinpa.ts +179 -0
- package/image.ts +335 -0
- package/index.ts +142 -0
- package/package.json +126 -0
- package/router.ts +94 -0
- package/state.ts +35 -0
- package/types.ts +17 -0
- package/utils.ts +99 -0
package/index.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { ConfigService, ScreenshotService } from "mioku";
|
|
2
|
+
import { definePlugin, type MiokiContext } from "mioki";
|
|
3
|
+
import {
|
|
4
|
+
cloneConfig,
|
|
5
|
+
DEFAULT_CONFIG,
|
|
6
|
+
normalizeImpactConfig,
|
|
7
|
+
type ImpactConfig,
|
|
8
|
+
} from "./configs/base";
|
|
9
|
+
import { initImpactDatabase, type ImpactDatabase } from "./db";
|
|
10
|
+
import { handleDajiao } from "./handlers/dajiao";
|
|
11
|
+
import { handleOpenModule } from "./handlers/open-module";
|
|
12
|
+
import { handlePk } from "./handlers/pk";
|
|
13
|
+
import { handleQueryInjection } from "./handlers/query-injection";
|
|
14
|
+
import { handleQueryJj } from "./handlers/queryjj";
|
|
15
|
+
import { handleRank } from "./handlers/rank";
|
|
16
|
+
import { handleSuo } from "./handlers/suo";
|
|
17
|
+
import { handleYinpa } from "./handlers/yinpa";
|
|
18
|
+
import type { HandlerContext } from "./handlers/types";
|
|
19
|
+
import { parseImpactCommand } from "./router";
|
|
20
|
+
import { createCooldownState } from "./state";
|
|
21
|
+
import { NOT_ALLOW_TEXT, isGroupEvent } from "./utils";
|
|
22
|
+
|
|
23
|
+
const impactPlugin = definePlugin({
|
|
24
|
+
name: "impact",
|
|
25
|
+
version: "1.0.0",
|
|
26
|
+
description: "让群友们眼前一黑的淫趴插件(牛子比拼),移植自 nonebot_plugin_impact",
|
|
27
|
+
|
|
28
|
+
async setup(ctx: MiokiContext) {
|
|
29
|
+
ctx.logger.info("impact 插件正在初始化...");
|
|
30
|
+
|
|
31
|
+
let db: ImpactDatabase;
|
|
32
|
+
try {
|
|
33
|
+
db = await initImpactDatabase();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
ctx.logger.error(`impact 数据库初始化失败: ${error}`);
|
|
36
|
+
return () => {
|
|
37
|
+
ctx.logger.info("impact 插件已卸载");
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cd = createCooldownState();
|
|
42
|
+
const screenshot = ctx.services?.screenshot as
|
|
43
|
+
| ScreenshotService
|
|
44
|
+
| undefined;
|
|
45
|
+
if (!screenshot) {
|
|
46
|
+
ctx.logger.warn(
|
|
47
|
+
"impact: screenshot 服务未启用, jj排行榜 / 注入查询历史图表将不可用",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let config: ImpactConfig = cloneConfig(DEFAULT_CONFIG);
|
|
52
|
+
const configService = ctx.services?.config as ConfigService | undefined;
|
|
53
|
+
if (configService) {
|
|
54
|
+
await configService.registerConfig("impact", "base", config);
|
|
55
|
+
const persisted = await configService.getConfig("impact", "base");
|
|
56
|
+
if (persisted) {
|
|
57
|
+
config = normalizeImpactConfig(persisted);
|
|
58
|
+
}
|
|
59
|
+
configService.onConfigChange("impact", "base", (next) => {
|
|
60
|
+
config = normalizeImpactConfig(next);
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
ctx.logger.warn("config 服务未加载,impact 插件将使用默认配置");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (config.isalive) {
|
|
67
|
+
ctx.cron("0 0 * * *", async () => {
|
|
68
|
+
try {
|
|
69
|
+
const touched = await db.punishInactiveUsers();
|
|
70
|
+
if (touched > 0) {
|
|
71
|
+
ctx.logger.info(`impact: 已对 ${touched} 个不活跃用户随机扣减长度`);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
ctx.logger.error(`impact 不活跃惩罚执行失败: ${error}`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
ctx.handle("message", async (event: any) => {
|
|
80
|
+
if (!isGroupEvent(event)) return;
|
|
81
|
+
const text = ctx.text(event);
|
|
82
|
+
if (!text) return;
|
|
83
|
+
|
|
84
|
+
const cmd = parseImpactCommand(text);
|
|
85
|
+
if (cmd.type === "none") return;
|
|
86
|
+
|
|
87
|
+
const h: HandlerContext = { ctx, db, cd, config, screenshot, event };
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// open_module 始终可用,不受群开关限制
|
|
91
|
+
if (cmd.type === "open_module") {
|
|
92
|
+
await handleOpenModule(h, cmd.enable);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 其余命令需要群开启了银趴
|
|
97
|
+
if (!db.isGroupAllowed(Number(event.group_id))) {
|
|
98
|
+
await event.reply(
|
|
99
|
+
[ctx.segment.at(Number(event.user_id)), ctx.segment.text(NOT_ALLOW_TEXT)],
|
|
100
|
+
false,
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
switch (cmd.type) {
|
|
106
|
+
case "pk":
|
|
107
|
+
return await handlePk(h);
|
|
108
|
+
case "dajiao":
|
|
109
|
+
return await handleDajiao(h);
|
|
110
|
+
case "suo":
|
|
111
|
+
return await handleSuo(h);
|
|
112
|
+
case "queryjj":
|
|
113
|
+
return await handleQueryJj(h);
|
|
114
|
+
case "rank":
|
|
115
|
+
return await handleRank(h);
|
|
116
|
+
case "yinpa":
|
|
117
|
+
return await handleYinpa(h, cmd.subject);
|
|
118
|
+
case "query_injection":
|
|
119
|
+
return await handleQueryInjection(h, cmd.mode);
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
ctx.logger.error(`impact 命令 ${cmd.type} 执行失败: ${error}`);
|
|
123
|
+
try {
|
|
124
|
+
await event.reply(
|
|
125
|
+
[ctx.segment.text(`淫趴插件出错了喵: ${String(error)}`)],
|
|
126
|
+
false,
|
|
127
|
+
);
|
|
128
|
+
} catch {
|
|
129
|
+
// 忽略二次失败
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
ctx.logger.info("impact 插件初始化完成");
|
|
135
|
+
|
|
136
|
+
return () => {
|
|
137
|
+
ctx.logger.info("impact 插件已卸载");
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
export default impactPlugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mioku-plugin-impact",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "让群友们眼前一黑的淫趴插件",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"mioku",
|
|
9
|
+
"impact",
|
|
10
|
+
"牛子"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/mioku-lab/mioku.git"
|
|
15
|
+
},
|
|
16
|
+
"mioku": {
|
|
17
|
+
"services": [
|
|
18
|
+
"screenshot",
|
|
19
|
+
"help",
|
|
20
|
+
"config"
|
|
21
|
+
],
|
|
22
|
+
"accessHooks": [
|
|
23
|
+
{
|
|
24
|
+
"id": "开启淫趴",
|
|
25
|
+
"match": "/^(?:开启|禁止|开始|关闭)(?:淫趴|银趴)\\s*$/",
|
|
26
|
+
"description": "群内开启或关闭淫趴功能"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "日透",
|
|
30
|
+
"match": "/^(?:日群友|透群友|日群主|透群主|日管理|透管理)(?:\\s|$|@)/",
|
|
31
|
+
"description": "日 / 透 系列命令"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "打胶",
|
|
35
|
+
"match": "/^(?:打胶|开导)\\s*$/",
|
|
36
|
+
"description": "打胶 / 开导"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "嗦牛子",
|
|
40
|
+
"match": "/^嗦牛子(?:\\s|$|@)/",
|
|
41
|
+
"description": "嗦自己或被 @ 群友的牛子"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "pk",
|
|
45
|
+
"match": "/^(?:pk|PK|对决)(?:\\s|@|$)/",
|
|
46
|
+
"description": "牛子对决"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "查询",
|
|
50
|
+
"match": "/^查询(?:\\s|@|$)/",
|
|
51
|
+
"description": "查询自己或被 @ 群友的牛子长度"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "注入查询",
|
|
55
|
+
"match": "/^(?:注入查询|摄入查询|射入查询)/",
|
|
56
|
+
"description": "查询当日或历史被注入量"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "jj排行榜",
|
|
60
|
+
"match": "/^(?:jj|JJ)(?:排行榜|排名|榜单|rank|RANK)(?:\\s|$)/",
|
|
61
|
+
"description": "牛子长度排行榜"
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"help": {
|
|
65
|
+
"title": "淫趴 ",
|
|
66
|
+
"description": "让群友们眼前一黑的牛子比拼插件",
|
|
67
|
+
"commands": [
|
|
68
|
+
{
|
|
69
|
+
"cmd": "开启淫趴 / 禁止淫趴",
|
|
70
|
+
"desc": "在本群开启或关闭淫趴功能(管理员/群主/主人可用)",
|
|
71
|
+
"usage": "开启淫趴 / 关闭银趴",
|
|
72
|
+
"role": "admin"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"cmd": "打胶 / 开导",
|
|
76
|
+
"desc": "给自己的牛子增加一段随机长度",
|
|
77
|
+
"role": "member"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"cmd": "嗦牛子",
|
|
81
|
+
"desc": "嗦自己或被 @ 的群友的牛子,增加随机长度",
|
|
82
|
+
"usage": "嗦牛子 / 嗦牛子 @某人",
|
|
83
|
+
"role": "member"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"cmd": "pk / 对决",
|
|
87
|
+
"desc": "和 @ 的群友进行牛子对决,胜者抢走败者的随机长度",
|
|
88
|
+
"usage": "pk @某人",
|
|
89
|
+
"role": "member"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"cmd": "查询",
|
|
93
|
+
"desc": "查询自己或 @ 的群友的牛子长度",
|
|
94
|
+
"usage": "查询 / 查询 @某人",
|
|
95
|
+
"role": "member"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"cmd": "jj排行榜",
|
|
99
|
+
"desc": "查看牛子长度前五与倒数五名的图表",
|
|
100
|
+
"usage": "jj排行榜 / jjrank",
|
|
101
|
+
"role": "member"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"cmd": "日群友 / 透群友 / 透群主 / 透管理",
|
|
105
|
+
"desc": "随机或定向选取一位幸运儿进行注入,按命令决定身份",
|
|
106
|
+
"usage": "日群友 / 透群友 @某人 / 透群主 / 透管理",
|
|
107
|
+
"role": "member"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"cmd": "注入查询",
|
|
111
|
+
"desc": "查询当日或全部历史被注入量,加 历史/全部 看走势图",
|
|
112
|
+
"usage": "注入查询 / 注入查询 历史 / 注入查询 @某人 全部",
|
|
113
|
+
"role": "member"
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"peerDependencies": {
|
|
119
|
+
"mioku": "^0.8.0",
|
|
120
|
+
"mioki": "^0.16.0"
|
|
121
|
+
},
|
|
122
|
+
"devDependencies": {
|
|
123
|
+
"mioku": "workspace:*",
|
|
124
|
+
"mioki": "^0.16.0"
|
|
125
|
+
}
|
|
126
|
+
}
|
package/router.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export type ImpactCommand =
|
|
2
|
+
| { type: "pk" }
|
|
3
|
+
| { type: "dajiao" }
|
|
4
|
+
| { type: "suo" }
|
|
5
|
+
| { type: "queryjj" }
|
|
6
|
+
| { type: "rank" }
|
|
7
|
+
| { type: "yinpa"; subject: "member" | "owner" | "admin" }
|
|
8
|
+
| { type: "open_module"; enable: boolean }
|
|
9
|
+
| { type: "query_injection"; mode: "today" | "history" }
|
|
10
|
+
| { type: "none" };
|
|
11
|
+
|
|
12
|
+
const YINPA_RE =
|
|
13
|
+
/^(?:日群友|透群友|日群主|透群主|日管理|透管理)(?:\s|$|@)/u;
|
|
14
|
+
|
|
15
|
+
const OPEN_MODULE_RE =
|
|
16
|
+
/^(?:开始银趴|关闭银趴|开启淫趴|禁止淫趴|开启银趴|禁止银趴)\s*$/u;
|
|
17
|
+
|
|
18
|
+
const RANK_HEADS = new Set([
|
|
19
|
+
"jj排行榜",
|
|
20
|
+
"jj排名",
|
|
21
|
+
"jj榜单",
|
|
22
|
+
"jjrank",
|
|
23
|
+
"JJ排行榜",
|
|
24
|
+
"JJ排名",
|
|
25
|
+
"JJ榜单",
|
|
26
|
+
"JJrank",
|
|
27
|
+
"JJRANK",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 把消息文本解析成 impact 命令。
|
|
32
|
+
* 仅看消息开头(去掉前导空白),命中失败返回 { type: "none" }。
|
|
33
|
+
*/
|
|
34
|
+
export function parseImpactCommand(text: string): ImpactCommand {
|
|
35
|
+
const head = text.replace(/^\s+/, "");
|
|
36
|
+
if (!head) return { type: "none" };
|
|
37
|
+
|
|
38
|
+
// 银趴开关:完整匹配
|
|
39
|
+
const open = head.match(OPEN_MODULE_RE);
|
|
40
|
+
if (open) {
|
|
41
|
+
const word = open[0];
|
|
42
|
+
const enable = word.startsWith("开启") || word.startsWith("开始");
|
|
43
|
+
return { type: "open_module", enable };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 透/日 系列
|
|
47
|
+
const yinpa = head.match(YINPA_RE);
|
|
48
|
+
if (yinpa) {
|
|
49
|
+
const word = yinpa[0];
|
|
50
|
+
const subject: "owner" | "admin" | "member" = word.includes("群主")
|
|
51
|
+
? "owner"
|
|
52
|
+
: word.includes("管理")
|
|
53
|
+
? "admin"
|
|
54
|
+
: "member";
|
|
55
|
+
return { type: "yinpa", subject };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// pk / 对决:要求形如 "pk @某人" 或 "pk"。具体的 at 校验放到 handler。
|
|
59
|
+
if (/^(pk|PK|对决)(\s|@|$)/.test(head)) {
|
|
60
|
+
return { type: "pk" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 打胶 / 开导:完整匹配
|
|
64
|
+
if (/^(打胶|开导)\s*$/.test(head)) {
|
|
65
|
+
return { type: "dajiao" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 嗦牛子
|
|
69
|
+
if (/^嗦牛子(\s|@|$)/.test(head)) {
|
|
70
|
+
return { type: "suo" };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 注入查询 / 摄入查询 / 射入查询
|
|
74
|
+
const inj = head.match(/^(?:注入查询|摄入查询|射入查询)(.*)$/u);
|
|
75
|
+
if (inj) {
|
|
76
|
+
const rest = inj[1] || "";
|
|
77
|
+
const mode = /历史|全部/.test(rest) ? "history" : "today";
|
|
78
|
+
return { type: "query_injection", mode };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 排行榜
|
|
82
|
+
for (const word of RANK_HEADS) {
|
|
83
|
+
if (head === word || head.startsWith(`${word} `) || head.startsWith(`${word}\n`)) {
|
|
84
|
+
return { type: "rank" };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 只匹配 "查询" + 空白/at/结束,避免误吞太多文本
|
|
89
|
+
if (/^查询(\s|@|$)/.test(head)) {
|
|
90
|
+
return { type: "queryjj" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { type: "none" };
|
|
94
|
+
}
|
package/state.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CooldownState } from "./types";
|
|
2
|
+
import { nowSeconds } from "./utils";
|
|
3
|
+
|
|
4
|
+
export function createCooldownState(): CooldownState {
|
|
5
|
+
return {
|
|
6
|
+
pk: new Map(),
|
|
7
|
+
dj: new Map(),
|
|
8
|
+
suo: new Map(),
|
|
9
|
+
fuck: new Map(),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 检查 CD 是否已过;返回 { ok, remaining },remaining 单位秒。
|
|
15
|
+
* ok 为 true 表示允许执行,调用方应自行 markUsed。
|
|
16
|
+
*/
|
|
17
|
+
export function checkCooldown(
|
|
18
|
+
bucket: Map<string, number>,
|
|
19
|
+
uid: string,
|
|
20
|
+
cdSeconds: number,
|
|
21
|
+
): { ok: boolean; remaining: number } {
|
|
22
|
+
const last = bucket.get(uid);
|
|
23
|
+
if (last == null) return { ok: true, remaining: 0 };
|
|
24
|
+
const elapsed = nowSeconds() - last;
|
|
25
|
+
if (elapsed >= cdSeconds) return { ok: true, remaining: 0 };
|
|
26
|
+
return { ok: false, remaining: cdSeconds - elapsed };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function markCooldown(bucket: Map<string, number>, uid: string): void {
|
|
30
|
+
bucket.set(uid, nowSeconds());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function clearCooldown(bucket: Map<string, number>, uid: string): void {
|
|
34
|
+
bucket.delete(uid);
|
|
35
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface EjaculationRecord {
|
|
2
|
+
/** YYYY-MM-DD */
|
|
3
|
+
date: string;
|
|
4
|
+
volume: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RankEntry {
|
|
8
|
+
userId: number;
|
|
9
|
+
jjLength: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CooldownState {
|
|
13
|
+
pk: Map<string, number>;
|
|
14
|
+
dj: Map<string, number>;
|
|
15
|
+
suo: Map<string, number>;
|
|
16
|
+
fuck: Map<string, number>;
|
|
17
|
+
}
|
package/utils.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { MiokiContext } from "mioki";
|
|
2
|
+
|
|
3
|
+
export const JJ_VARIABLES = ["牛子", "牛牛", "丁丁", "JJ"] as const;
|
|
4
|
+
|
|
5
|
+
export const NOT_ALLOW_TEXT =
|
|
6
|
+
'群内还未开启淫趴游戏, 请管理员或群主发送"开启淫趴"以开启该功能';
|
|
7
|
+
|
|
8
|
+
export function nowSeconds(): number {
|
|
9
|
+
return Math.floor(Date.now() / 1000);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getToday(): string {
|
|
13
|
+
const d = new Date();
|
|
14
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
15
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function pickJj(): string {
|
|
19
|
+
return JJ_VARIABLES[Math.floor(Math.random() * JJ_VARIABLES.length)];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 获取一个随机数:0.1 概率落在 [1, 2),0.9 概率落在 [0, 1)。保留 3 位小数。
|
|
24
|
+
*/
|
|
25
|
+
export function getRandomNum(): number {
|
|
26
|
+
const seed = Math.random();
|
|
27
|
+
const value = seed > 0.1 ? Math.random() : 1 + Math.random();
|
|
28
|
+
return Math.round(value * 1000) / 1000;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function roundTo(value: number, digits: number): number {
|
|
32
|
+
const f = 10 ** digits;
|
|
33
|
+
return Math.round(value * f) / f;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 提取消息里第一个 @ 的 QQ 号,忽略 @全体。
|
|
38
|
+
* 找不到返回 undefined。
|
|
39
|
+
*/
|
|
40
|
+
export function getAtUserId(message: any): number | undefined {
|
|
41
|
+
if (!Array.isArray(message)) return undefined;
|
|
42
|
+
for (const seg of message) {
|
|
43
|
+
if (seg?.type !== "at") continue;
|
|
44
|
+
const raw = seg?.qq ?? seg?.data?.qq;
|
|
45
|
+
if (raw == null || raw === "all") continue;
|
|
46
|
+
const qq = Number(raw);
|
|
47
|
+
if (Number.isFinite(qq)) return qq;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isGroupEvent(event: any): boolean {
|
|
53
|
+
return event?.message_type === "group" && event?.group_id != null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isGroupAdmin(event: any): boolean {
|
|
57
|
+
const role = event?.sender?.role;
|
|
58
|
+
return role === "admin" || role === "owner";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getSenderName(event: any): string {
|
|
62
|
+
const card = String(event?.sender?.card || "").trim();
|
|
63
|
+
if (card) return card;
|
|
64
|
+
const nickname = String(event?.sender?.nickname || "").trim();
|
|
65
|
+
if (nickname) return nickname;
|
|
66
|
+
return String(event?.user_id ?? "群友");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function getStrangerNickname(
|
|
70
|
+
ctx: MiokiContext,
|
|
71
|
+
selfId: number,
|
|
72
|
+
userId: number,
|
|
73
|
+
): Promise<string> {
|
|
74
|
+
try {
|
|
75
|
+
const info = (await ctx
|
|
76
|
+
.pickBot(selfId)
|
|
77
|
+
.api("get_stranger_info", { user_id: userId, no_cache: false })) as
|
|
78
|
+
| { nickname?: string }
|
|
79
|
+
| undefined;
|
|
80
|
+
const name = String(info?.nickname || "").trim();
|
|
81
|
+
if (name) return name;
|
|
82
|
+
} catch {
|
|
83
|
+
// 接口不可用就回退到 QQ 号
|
|
84
|
+
}
|
|
85
|
+
return String(userId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getAvatarUrl(userId: number): string {
|
|
89
|
+
return `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function escapeHtml(value: string): string {
|
|
93
|
+
return String(value)
|
|
94
|
+
.replace(/&/g, "&")
|
|
95
|
+
.replace(/</g, "<")
|
|
96
|
+
.replace(/>/g, ">")
|
|
97
|
+
.replace(/"/g, """)
|
|
98
|
+
.replace(/'/g, "'");
|
|
99
|
+
}
|