koishi-plugin-forward-hime 1.3.9 → 1.4.0-alpha.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 CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 xianii
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2024 xianii
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/lib/cache.js ADDED
@@ -0,0 +1,60 @@
1
+ import { logger } from "./logger";
2
+ let ctx;
3
+ let cacheTimeout = 86400 * 1000;
4
+ function cacheNotEnabled() {
5
+ if (!ctx.cache) {
6
+ return true;
7
+ }
8
+ return false;
9
+ }
10
+ export function msgCacheInit(context, cfg) {
11
+ ctx = context;
12
+ cacheTimeout = 60 * cfg.CacheTimeout * 1000;
13
+ }
14
+ export async function msgCache(mc) {
15
+ if (cacheNotEnabled()) {
16
+ return;
17
+ }
18
+ const key = mc.guild + ":" + mc.msgid;
19
+ logger.debug(`[message-cache] ${mc.platform} ${key}`);
20
+ await ctx.cache.set("msgCache", key, mc, cacheTimeout);
21
+ }
22
+ export async function msgCacheDelete(key) {
23
+ if (cacheNotEnabled()) {
24
+ return;
25
+ }
26
+ await ctx.cache.delete("msgCache", key);
27
+ }
28
+ export async function msgCacheFindByKey(key) {
29
+ if (cacheNotEnabled()) {
30
+ return;
31
+ }
32
+ return await ctx.cache.get("msgCache", key);
33
+ }
34
+ export async function msgCacheFindByUUID(uuid) {
35
+ if (cacheNotEnabled()) {
36
+ return;
37
+ }
38
+ let caches = await ctx.cache.entries("msgCache");
39
+ let result = [];
40
+ for await (const item of caches) {
41
+ if (item[1].uuid === uuid) {
42
+ result.push(item);
43
+ }
44
+ }
45
+ return result;
46
+ }
47
+ export async function msgCacheGetLocalIDByUUID(node, uuid) {
48
+ if (cacheNotEnabled()) {
49
+ return;
50
+ }
51
+ let caches = await ctx.cache.entries("msgCache");
52
+ for await (const item of caches) {
53
+ if (item[1].uuid === uuid &&
54
+ item[1].platform === node.Platform &&
55
+ item[1].guild === node.Guild &&
56
+ item[1].bot === node.BotID) {
57
+ return item[1].msgid;
58
+ }
59
+ }
60
+ }
package/lib/config.d.ts CHANGED
@@ -11,6 +11,12 @@ export interface ConfigSet {
11
11
  Newline: boolean;
12
12
  };
13
13
  DefaultFallbackMsgPrefix: string;
14
+ MediaRelay: {
15
+ Enabled: boolean;
16
+ CacheMinutes: number;
17
+ RequestTimeoutSec: number;
18
+ MaxFileSizeMB: number;
19
+ };
14
20
  CacheTimeout: number;
15
21
  ForwardGroups: {
16
22
  Nodes: ForwardNode[];
package/lib/config.js ADDED
@@ -0,0 +1,56 @@
1
+ import { Schema } from "koishi";
2
+ export const createConfig = () => Schema.intersect([
3
+ Schema.object({
4
+ CacheTimeout: Schema.number()
5
+ .description("消息缓存时间(分钟)- 影响跨平台删除与回复等功能")
6
+ .default(120)
7
+ .min(120)
8
+ .max(1440 * 7),
9
+ }).description("消息缓存"),
10
+ Schema.object({
11
+ DefaultDecorator: Schema.object({
12
+ Prefix: Schema.string()
13
+ .description("转发前缀")
14
+ .default("${username} 转发自 ${platform}:"),
15
+ Newline: Schema.boolean().description("是否换行").default(true),
16
+ }).description("默认装饰器"),
17
+ DefaultFallbackMsgPrefix: Schema.string()
18
+ .description("默认Fallback消息前缀")
19
+ .default("[消息降级] "),
20
+ MediaRelay: Schema.object({
21
+ Enabled: Schema.boolean()
22
+ .description("启用媒体中转(下载后再转发)")
23
+ .default(true),
24
+ CacheMinutes: Schema.number()
25
+ .description("媒体暂存时间(分钟)")
26
+ .default(10)
27
+ .min(1)
28
+ .max(180),
29
+ RequestTimeoutSec: Schema.number()
30
+ .description("媒体下载超时时间(秒)")
31
+ .default(15)
32
+ .min(3)
33
+ .max(120),
34
+ MaxFileSizeMB: Schema.number()
35
+ .description("允许中转的媒体最大大小(MB)")
36
+ .default(20)
37
+ .min(1)
38
+ .max(200),
39
+ }).description("媒体中转"),
40
+ }).description("转发格式"),
41
+ Schema.object({
42
+ ForwardGroups: Schema.array(Schema.object({
43
+ Note: Schema.string().description("互通组备注"),
44
+ Nodes: Schema.array(Schema.object({
45
+ Platform: Schema.string()
46
+ .role("")
47
+ .required()
48
+ .description("平台"),
49
+ Guild: Schema.string().required().description("频道ID"),
50
+ BotID: Schema.string().required().description("机器人ID"),
51
+ }))
52
+ .role("table")
53
+ .description("转发节点"),
54
+ })).description("互通转发组"),
55
+ }).description("互通组"),
56
+ ]);
@@ -11,4 +11,5 @@ export declare function defaultMiddleware(session: Session): ForwardMsg;
11
11
  export declare function MsgMiddlewareCache(session: Session): Promise<void>;
12
12
  export declare function MsgDecorator(session: Session, node: ForwardNode): Promise<any>;
13
13
  export declare function MsgDecoratorFallback(session: Session, node: ForwardNode): Promise<Element[]>;
14
+ export declare function MsgDecoratorFallbackReason(session: Session, node: ForwardNode, reason: string): Promise<Element[]>;
14
15
  export {};
@@ -0,0 +1,159 @@
1
+ import { h } from "koishi";
2
+ import { MsgUUIDFromSession } from "./message";
3
+ import { relayForwardContent, relayInit } from "./relay";
4
+ import * as decorator from "./decorators";
5
+ let defaultPrefix = "";
6
+ let defaultFallback = "";
7
+ let defaultPrefixNewline = true;
8
+ export function decoratorInit(cfg) {
9
+ defaultPrefix = cfg.DefaultDecorator.Prefix;
10
+ defaultPrefixNewline = cfg.DefaultDecorator.Newline;
11
+ defaultFallback = cfg.DefaultFallbackMsgPrefix;
12
+ relayInit(cfg);
13
+ }
14
+ function renderTemplate(template, data) {
15
+ return template.replace(/\$\{(\w+)\}/g, (_, key) => {
16
+ return data[key] !== undefined ? data[key] : `$\{${key}}`; // 未找到则保留原模板
17
+ });
18
+ }
19
+ export function defaultDecorator({ head, content }) {
20
+ let msg = [];
21
+ msg = msg.concat(head, content);
22
+ return msg;
23
+ }
24
+ export function defaultMiddleware(session) {
25
+ let head = [h("span", renderTemplate(defaultPrefix, session))];
26
+ if (defaultPrefixNewline) {
27
+ head.push(h("br"));
28
+ }
29
+ return { head: head, content: session.elements };
30
+ }
31
+ const localDecorators = [atTranslator, quoteTranslator];
32
+ let msgMiddleCache = [];
33
+ function msgMiddleCacheAppend(msg) {
34
+ msgMiddleCache.push(msg);
35
+ if (msgMiddleCache.length > 16) {
36
+ msgMiddleCache.shift();
37
+ }
38
+ logger.debug(`[msgMiddleCache]`, msgMiddleCache.length);
39
+ }
40
+ function msgMiddleCacheFind(uuid) {
41
+ for (const item of msgMiddleCache) {
42
+ if (item.UUID === uuid) {
43
+ return item.msg;
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ function MsgToMiddleware(session) {
49
+ let elems = { head: [], content: [] };
50
+ let _platform_in = decorator[session.platform];
51
+ if (_platform_in && typeof _platform_in.Middleware === "function") {
52
+ elems = _platform_in.Middleware(session);
53
+ }
54
+ else {
55
+ elems = defaultMiddleware(session);
56
+ }
57
+ return elems;
58
+ }
59
+ export async function MsgMiddlewareCache(session) {
60
+ let elems = MsgToMiddleware(session);
61
+ msgMiddleCacheAppend({ UUID: MsgUUIDFromSession(session), msg: elems });
62
+ logger.debug(`[msgMiddleCache] CACHED`);
63
+ }
64
+ export async function MsgDecorator(session, node) {
65
+ let elems;
66
+ let _platform_out = decorator[node.Platform];
67
+ let elemCache = msgMiddleCacheFind(MsgUUIDFromSession(session));
68
+ if (elemCache) {
69
+ logger.debug(`[msgMiddleCache] HIT`);
70
+ elems = elemCache;
71
+ }
72
+ else {
73
+ logger.debug(`[msgMiddleCache] MISSED`);
74
+ elems = MsgToMiddleware(session);
75
+ }
76
+ for (const fn of localDecorators) {
77
+ elems = (await fn(session, node, elems));
78
+ }
79
+ elems = {
80
+ head: elems.head,
81
+ content: await relayForwardContent(elems.content),
82
+ };
83
+ if (_platform_out && typeof _platform_out.Decorator === "function") {
84
+ return _platform_out.Decorator(elems);
85
+ }
86
+ else {
87
+ return defaultDecorator(elems);
88
+ }
89
+ }
90
+ function defaultDecoratorFallback({ head, content }, reason) {
91
+ let msg = [];
92
+ let newContent = [];
93
+ newContent.push(h("span", reason || defaultFallback));
94
+ for (const key in content) {
95
+ if (["img", "audio", "video", "file"].includes(content[key].type)) {
96
+ newContent.push(h("span", ` [${content[key].type}] `));
97
+ }
98
+ else {
99
+ newContent.push(content[key]);
100
+ }
101
+ }
102
+ msg = msg.concat(head, newContent);
103
+ return msg;
104
+ }
105
+ export async function MsgDecoratorFallback(session, node) {
106
+ let elems = { head: [], content: [] };
107
+ elems = defaultMiddleware(session);
108
+ for (const fn of localDecorators) {
109
+ elems = (await fn(session, node, elems));
110
+ }
111
+ return defaultDecoratorFallback(elems);
112
+ }
113
+ export async function MsgDecoratorFallbackReason(session, node, reason) {
114
+ let elems = { head: [], content: [] };
115
+ elems = defaultMiddleware(session);
116
+ for (const fn of localDecorators) {
117
+ elems = (await fn(session, node, elems));
118
+ }
119
+ return defaultDecoratorFallback(elems, reason);
120
+ }
121
+ async function atTranslator(session, _, { head, content }) {
122
+ const newMsg = await new Promise((resolve, reject) => {
123
+ let newContent = [];
124
+ for (const key in content) {
125
+ const element = content[key];
126
+ if (element.type === "at") {
127
+ if (element.attrs.id !== session.selfId) {
128
+ // Note: onebot-qq at 无昵称
129
+ newContent.push(h("span", `@${element.attrs.name || element.attrs.id}`));
130
+ }
131
+ }
132
+ else {
133
+ newContent.push(element);
134
+ }
135
+ }
136
+ resolve({ head: head, content: newContent });
137
+ });
138
+ return newMsg;
139
+ }
140
+ import { logger } from "./logger";
141
+ import { msgCacheFindByKey, msgCacheGetLocalIDByUUID } from "./cache";
142
+ async function quoteTranslator(session, node, { head, content }) {
143
+ if (!session.quote || !session.quote.id) {
144
+ return { head: head, content: content };
145
+ }
146
+ const key = session.channelId + ":" + session.quote.id;
147
+ const cache = await msgCacheFindByKey(key);
148
+ if (cache) {
149
+ let msgid = await msgCacheGetLocalIDByUUID(node, cache.uuid);
150
+ if (msgid) {
151
+ let newHead = [h("quote", { id: msgid })].concat(head);
152
+ return { head: newHead, content: content };
153
+ }
154
+ else {
155
+ logger.error(`[msgCacheGetLocalIDByUUID] ${cache.uuid} not found`);
156
+ }
157
+ }
158
+ return { head: head, content: content };
159
+ }
@@ -0,0 +1,2 @@
1
+ export { default as kook } from "./kook";
2
+ export { default as onebot } from "./onebot";
@@ -0,0 +1,20 @@
1
+ import { h } from "koishi";
2
+ import { Buffer } from "buffer";
3
+ const transparentPixel = Buffer.from("R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", "base64");
4
+ function Decorator({ head, content }) {
5
+ let msg = [];
6
+ let hasImage = false;
7
+ for (const key in content) {
8
+ if (content[key].type === "image") {
9
+ hasImage = true;
10
+ }
11
+ }
12
+ msg = msg.concat(head, h("br"), content);
13
+ if (!hasImage) {
14
+ msg = msg.concat(h.image(transparentPixel, "image/gif"));
15
+ }
16
+ return msg;
17
+ }
18
+ export default {
19
+ Decorator,
20
+ };
@@ -0,0 +1,81 @@
1
+ import { h } from "koishi";
2
+ import { defaultMiddleware } from "../decorator";
3
+ function cqJsonTranslator(content, elem) {
4
+ let json = JSON.parse(elem.attrs.data);
5
+ let hit = 0;
6
+ if (json) {
7
+ if (json.prompt) {
8
+ hit += 1;
9
+ content.push(h("p", `${json.prompt}`));
10
+ }
11
+ if (json.meta && json.meta.detail_1) {
12
+ if (json.meta.detail_1.preview) {
13
+ hit += 1;
14
+ content.push(h.image(json.meta.detail_1.preview, "image/jpeg"));
15
+ }
16
+ if (json.meta.detail_1.qqdocurl) {
17
+ hit += 1;
18
+ content.push(h("span", `${json.meta.detail_1.qqdocurl}`));
19
+ }
20
+ }
21
+ }
22
+ if (hit === 0) {
23
+ content.push(h("span", `[CQ:JSON消息]`));
24
+ }
25
+ }
26
+ const IdNameCache = new Map();
27
+ function getNameFromQQat(elem) {
28
+ const name = elem.attrs.name;
29
+ if (name) {
30
+ return name;
31
+ }
32
+ const id = elem.attrs.id;
33
+ if (IdNameCache.has(id)) {
34
+ return IdNameCache.get(id);
35
+ }
36
+ else {
37
+ return id;
38
+ }
39
+ }
40
+ function Middleware(session) {
41
+ const platform = guessPlatform(session);
42
+ let sessionTemp = session;
43
+ sessionTemp.platform = platform;
44
+ let head = defaultMiddleware(sessionTemp).head;
45
+ let newContent = [];
46
+ if (platform === "QQ") {
47
+ IdNameCache.set(session.userId, session.username);
48
+ for (const key in session.elements) {
49
+ switch (session.elements[key].type) {
50
+ case "at":
51
+ newContent.push(h("span", `@${getNameFromQQat(session.elements[key])}`));
52
+ break;
53
+ case "forward":
54
+ newContent.push(h("span", `[CQ:转发消息]`));
55
+ break;
56
+ case "json":
57
+ cqJsonTranslator(newContent, session.elements[key]);
58
+ break;
59
+ case "mface":
60
+ newContent.push(h.image(session.elements[key].attrs.url));
61
+ break;
62
+ default:
63
+ newContent.push(session.elements[key]);
64
+ break;
65
+ }
66
+ }
67
+ return { head: head, content: newContent };
68
+ }
69
+ return { head: head, content: session.elements };
70
+ }
71
+ function guessPlatform(session) {
72
+ if (session.event.user && session.event.user.avatar) {
73
+ if (session.event.user.avatar.includes("q.qlogo.cn")) {
74
+ return "QQ";
75
+ }
76
+ }
77
+ return session.platform;
78
+ }
79
+ export default {
80
+ Middleware,
81
+ };