@alemonjs/bubble 2.1.0 → 2.1.2
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 +6 -33
- package/lib/config.d.ts +1 -0
- package/lib/format.d.ts +4 -0
- package/lib/format.js +84 -0
- package/lib/index.js +5 -2
- package/lib/send.js +110 -92
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# @alemonjs/bubble
|
|
2
2
|
|
|
3
|
-
Bubble 平台适配器 for AlemonJS
|
|
4
|
-
|
|
5
3
|
[文档 https://bubble.alemonjs.com/developer/docs](https://bubble.alemonjs.com/developer/docs)
|
|
6
4
|
|
|
7
5
|
## 安装
|
|
@@ -44,35 +42,10 @@ bubble:
|
|
|
44
42
|
|
|
45
43
|
# 客户端名称(可选,默认: alemonjs-bot)
|
|
46
44
|
clientName: 'alemonjs-bot'
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## 功能特性
|
|
50
|
-
|
|
51
|
-
- ✅ WebSocket Gateway 实时事件推送
|
|
52
|
-
- ✅ 完整的 HTTP API 接口支持
|
|
53
|
-
- ✅ 文件上传功能
|
|
54
|
-
- ✅ 类型安全的 TypeScript 支持
|
|
55
|
-
- ✅ 自动重连机制
|
|
56
|
-
- ✅ 事件订阅管理
|
|
57
45
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
- `MESSAGE_CREATE` - 频道新消息创建
|
|
65
|
-
- `MESSAGE_UPDATE` - 频道消息更新
|
|
66
|
-
- `MESSAGE_DELETE` - 频道消息删除
|
|
67
|
-
- `MESSAGE_UNPIN` - 频道消息取消置顶
|
|
68
|
-
- `DM_MESSAGE_CREATE` - 私聊新消息创建
|
|
69
|
-
- `DM_MESSAGE_UPDATE` - 私聊消息更新
|
|
70
|
-
- `DM_MESSAGE_DELETE` - 私聊消息删除
|
|
71
|
-
- `DM_MESSAGE_UNPIN` - 私聊消息取消置顶
|
|
72
|
-
- `GUILD_MEMBER_ADD` - 成员加入服务器
|
|
73
|
-
- `GUILD_MEMBER_UPDATE` - 成员更新
|
|
74
|
-
- `GUILD_MEMBER_REMOVE` - 成员离开服务器
|
|
75
|
-
- `BOT_READY` - 机器人就绪
|
|
76
|
-
- `EVENTS_SUBSCRIBED` - 事件订阅成功
|
|
77
|
-
- `EVENTS_UNSUBSCRIBED` - 事件取消订阅成功
|
|
78
|
-
- `SUBSCRIBE_DENIED` - 订阅被拒绝
|
|
46
|
+
# 隐藏不支持的消息类型(可选,默认: false)
|
|
47
|
+
# 开启后,转为文本时不可读内容(如 [视频]、[音频]、[图片]、[附件] 等占位符)将被置空
|
|
48
|
+
# 可读内容(如标题、按钮文本、链接等)仍会保留为纯文本
|
|
49
|
+
# 转换后内容为空时,将跳过发送并输出 info 日志
|
|
50
|
+
hideUnsupported: true
|
|
51
|
+
```
|
package/lib/config.d.ts
CHANGED
package/lib/format.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DataEnums, DataMarkDown } from 'alemonjs';
|
|
2
|
+
export declare const markdownToBubbleText: (items: DataMarkDown["value"]) => string;
|
|
3
|
+
export declare const markdownRawToBubbleText: (raw: string, hideUnsupported?: boolean) => string;
|
|
4
|
+
export declare const dataEnumToBubbleText: (item: DataEnums, hideUnsupported?: boolean) => string;
|
package/lib/format.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const markdownToBubbleText = (items) => {
|
|
2
|
+
return items
|
|
3
|
+
.map(item => {
|
|
4
|
+
switch (item.type) {
|
|
5
|
+
case 'MD.text':
|
|
6
|
+
return item.value;
|
|
7
|
+
case 'MD.title':
|
|
8
|
+
return `# ${item.value}\n`;
|
|
9
|
+
case 'MD.subtitle':
|
|
10
|
+
return `## ${item.value}\n`;
|
|
11
|
+
case 'MD.bold':
|
|
12
|
+
return `**${item.value}**`;
|
|
13
|
+
case 'MD.italic':
|
|
14
|
+
case 'MD.italicStar':
|
|
15
|
+
return `*${item.value}*`;
|
|
16
|
+
case 'MD.strikethrough':
|
|
17
|
+
return `~~${item.value}~~`;
|
|
18
|
+
case 'MD.link': {
|
|
19
|
+
const v = item.value;
|
|
20
|
+
return `[${v.text}](${v.url})`;
|
|
21
|
+
}
|
|
22
|
+
case 'MD.image':
|
|
23
|
+
return ``;
|
|
24
|
+
case 'MD.list':
|
|
25
|
+
return (item.value
|
|
26
|
+
.map(li => {
|
|
27
|
+
if (typeof li.value === 'object') {
|
|
28
|
+
return `${li.value.index}. ${li.value.text ?? ''}`;
|
|
29
|
+
}
|
|
30
|
+
return `- ${li.value}`;
|
|
31
|
+
})
|
|
32
|
+
.join('\n') + '\n');
|
|
33
|
+
case 'MD.blockquote':
|
|
34
|
+
return `> ${item.value}\n`;
|
|
35
|
+
case 'MD.divider':
|
|
36
|
+
return '\n————————\n';
|
|
37
|
+
case 'MD.newline':
|
|
38
|
+
return '\n';
|
|
39
|
+
case 'MD.code': {
|
|
40
|
+
const lang = item?.options?.language || '';
|
|
41
|
+
return `\`\`\`${lang}\n${item.value}\n\`\`\`\n`;
|
|
42
|
+
}
|
|
43
|
+
case 'MD.mention':
|
|
44
|
+
if (item.value === 'everyone') {
|
|
45
|
+
return '<@everyone>';
|
|
46
|
+
}
|
|
47
|
+
return `<@${item.value ?? ''}>`;
|
|
48
|
+
case 'MD.content':
|
|
49
|
+
return item.value;
|
|
50
|
+
case 'MD.button': {
|
|
51
|
+
const options = item?.options;
|
|
52
|
+
const autoEnter = options?.autoEnter ?? false;
|
|
53
|
+
const label = typeof item.value === 'object' ? item.value.title : item.value;
|
|
54
|
+
const command = options?.data || label;
|
|
55
|
+
return `<btn variant="borderless" command="${command}" enter="${String(autoEnter)}" >${label}</btn> `;
|
|
56
|
+
}
|
|
57
|
+
default:
|
|
58
|
+
return String(item?.value ?? '');
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
.join('');
|
|
62
|
+
};
|
|
63
|
+
const markdownRawToBubbleText = (raw, hideUnsupported) => {
|
|
64
|
+
if (hideUnsupported) {
|
|
65
|
+
return raw.replace(/!\[([^\]]*)\]\([^)]*\)/g, '');
|
|
66
|
+
}
|
|
67
|
+
return raw;
|
|
68
|
+
};
|
|
69
|
+
const dataEnumToBubbleText = (item, hideUnsupported) => {
|
|
70
|
+
switch (item.type) {
|
|
71
|
+
case 'MarkdownOriginal':
|
|
72
|
+
return markdownRawToBubbleText(String(item.value), hideUnsupported);
|
|
73
|
+
case 'Attachment':
|
|
74
|
+
return hideUnsupported ? '' : `[附件${item.options?.filename ? ': ' + item.options.filename : ''}]`;
|
|
75
|
+
case 'Audio':
|
|
76
|
+
return hideUnsupported ? '' : '[音频]';
|
|
77
|
+
case 'Video':
|
|
78
|
+
return hideUnsupported ? '' : '[视频]';
|
|
79
|
+
default:
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export { dataEnumToBubbleText, markdownRawToBubbleText, markdownToBubbleText };
|
package/lib/index.js
CHANGED
|
@@ -14,9 +14,12 @@ const main = () => {
|
|
|
14
14
|
const client = new BubbleClient();
|
|
15
15
|
void client.connect();
|
|
16
16
|
let botId = '';
|
|
17
|
-
client
|
|
17
|
+
client
|
|
18
|
+
.getMe()
|
|
19
|
+
.then(res => {
|
|
18
20
|
botId = String(res?.id ?? '');
|
|
19
|
-
})
|
|
21
|
+
})
|
|
22
|
+
.catch(() => { });
|
|
20
23
|
const createUserAvatar = (_UserId, avatar) => {
|
|
21
24
|
if (!avatar) {
|
|
22
25
|
return '';
|
package/lib/send.js
CHANGED
|
@@ -1,10 +1,102 @@
|
|
|
1
1
|
import { createResult, ResultCode } from 'alemonjs';
|
|
2
2
|
import { readFileSync } from 'fs';
|
|
3
|
+
import { dataEnumToBubbleText } from './format.js';
|
|
4
|
+
import { getBubbleConfig } from './config.js';
|
|
3
5
|
|
|
4
6
|
const ImageURLToBuffer = async (url) => {
|
|
5
7
|
const arrayBuffer = await fetch(url).then(res => res.arrayBuffer());
|
|
6
8
|
return Buffer.from(arrayBuffer);
|
|
7
9
|
};
|
|
10
|
+
const buildBubbleMdContent = (mdAndButtons) => {
|
|
11
|
+
let contentMd = '';
|
|
12
|
+
if (mdAndButtons && mdAndButtons.length > 0) {
|
|
13
|
+
mdAndButtons.forEach(item => {
|
|
14
|
+
if (item.type === 'Markdown' && typeof item.value !== 'string') {
|
|
15
|
+
const md = item.value;
|
|
16
|
+
const map = {
|
|
17
|
+
'MD.title': value => `# ${value}`,
|
|
18
|
+
'MD.subtitle': value => `## ${value}`,
|
|
19
|
+
'MD.text': value => `${value} `,
|
|
20
|
+
'MD.bold': value => `**${value}** `,
|
|
21
|
+
'MD.divider': () => '\n————————\n',
|
|
22
|
+
'MD.italic': value => `_${value}_ `,
|
|
23
|
+
'MD.italicStar': value => `*${value}* `,
|
|
24
|
+
'MD.strikethrough': value => `~~${value}~~ `,
|
|
25
|
+
'MD.blockquote': value => `\n> ${value}`,
|
|
26
|
+
'MD.newline': () => '\n',
|
|
27
|
+
'MD.link': value => `[🔗${value.text}](${value.url}) `,
|
|
28
|
+
'MD.image': value => `\n\n`,
|
|
29
|
+
'MD.mention': (value, options) => {
|
|
30
|
+
const { belong } = options || {};
|
|
31
|
+
if (value === 'everyone' || value === 'all' || value === '' || typeof value !== 'string') {
|
|
32
|
+
return '<@everyone> ';
|
|
33
|
+
}
|
|
34
|
+
if (belong === 'user') {
|
|
35
|
+
return `<@${value}> `;
|
|
36
|
+
}
|
|
37
|
+
else if (belong === 'channel') {
|
|
38
|
+
return `<#${value}> `;
|
|
39
|
+
}
|
|
40
|
+
return '';
|
|
41
|
+
},
|
|
42
|
+
'MD.button': (value, options) => {
|
|
43
|
+
const autoEnter = options?.autoEnter ?? false;
|
|
44
|
+
const label = typeof value === 'object' ? value.title : value;
|
|
45
|
+
const command = options?.data || label;
|
|
46
|
+
return `<btn variant="borderless" command="${command}" enter="${String(autoEnter)}" >${label}</btn> `;
|
|
47
|
+
},
|
|
48
|
+
'MD.content': value => `${value}`
|
|
49
|
+
};
|
|
50
|
+
md.forEach(line => {
|
|
51
|
+
if (map[line.type]) {
|
|
52
|
+
const value = 'value' in line ? line.value : undefined;
|
|
53
|
+
const options = 'options' in line ? line.options : {};
|
|
54
|
+
contentMd += map[line.type](value, options);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (line.type === 'MD.list') {
|
|
58
|
+
const listStr = line.value.map(listItem => {
|
|
59
|
+
if (typeof listItem.value === 'object') {
|
|
60
|
+
return `\n${listItem.value.index}. ${listItem.value.text}`;
|
|
61
|
+
}
|
|
62
|
+
return `\n- ${listItem.value}`;
|
|
63
|
+
});
|
|
64
|
+
contentMd += `${listStr.join('')}\n`;
|
|
65
|
+
}
|
|
66
|
+
else if (line.type === 'MD.code') {
|
|
67
|
+
const language = line?.options?.language || '';
|
|
68
|
+
contentMd += `\n\`\`\`${language}\n${line.value}\n\`\`\`\n`;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const value = line['value'] || '';
|
|
72
|
+
contentMd += String(value);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else if (item.type === 'BT.group' && item.value.length > 0 && typeof item.value !== 'string') {
|
|
77
|
+
contentMd += `<box classWind="mt-2" variant="borderless" >${item.value
|
|
78
|
+
?.map(row => {
|
|
79
|
+
const val = row.value;
|
|
80
|
+
if (val.length === 0) {
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
return `<flex>${val
|
|
84
|
+
.map(button => {
|
|
85
|
+
const value = button?.value || {};
|
|
86
|
+
const options = button.options;
|
|
87
|
+
const autoEnter = options?.autoEnter ?? false;
|
|
88
|
+
const label = value;
|
|
89
|
+
const command = options?.data || label;
|
|
90
|
+
return `<btn command="${command}" enter="${String(autoEnter)}" >${label}</btn>`;
|
|
91
|
+
})
|
|
92
|
+
.join('')}</flex>`;
|
|
93
|
+
})
|
|
94
|
+
.join('')}</box>`;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return contentMd;
|
|
99
|
+
};
|
|
8
100
|
const sendToRoom = async (client, param, val) => {
|
|
9
101
|
try {
|
|
10
102
|
if (!val || val.length <= 0) {
|
|
@@ -15,6 +107,13 @@ const sendToRoom = async (client, param, val) => {
|
|
|
15
107
|
const messageId = param?.message_id ? String(param?.message_id) : undefined;
|
|
16
108
|
const images = val.filter(item => item.type === 'Image' || item.type === 'ImageURL' || item.type === 'ImageFile');
|
|
17
109
|
const mdAndButtons = val.filter(item => item.type === 'Markdown' || item.type === 'BT.group');
|
|
110
|
+
const nativeTypes = new Set(['Image', 'ImageURL', 'ImageFile', 'Markdown', 'BT.group', 'Mention', 'Text', 'Link']);
|
|
111
|
+
const unsupportedItems = val.filter(item => !nativeTypes.has(item.type));
|
|
112
|
+
const hide = getBubbleConfig().hideUnsupported === true;
|
|
113
|
+
const fallbackText = unsupportedItems
|
|
114
|
+
.map(item => dataEnumToBubbleText(item, hide))
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.join('\n');
|
|
18
117
|
const content = val
|
|
19
118
|
.filter(item => item.type === 'Mention' || item.type === 'Text' || item.type === 'Link')
|
|
20
119
|
.map(item => {
|
|
@@ -51,6 +150,12 @@ const sendToRoom = async (client, param, val) => {
|
|
|
51
150
|
return '';
|
|
52
151
|
})
|
|
53
152
|
.join('');
|
|
153
|
+
const contentMd = buildBubbleMdContent(mdAndButtons);
|
|
154
|
+
const finalContent = [content, contentMd, fallbackText].filter(Boolean).join('\n');
|
|
155
|
+
if (hide && !finalContent && images.length <= 0) {
|
|
156
|
+
logger.info('[bubble] hideUnsupported: 消息内容转换后为空,跳过发送');
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
54
159
|
if (images.length > 0) {
|
|
55
160
|
let bufferData = null;
|
|
56
161
|
for (let i = 0; i < images.length; i++) {
|
|
@@ -89,7 +194,7 @@ const sendToRoom = async (client, param, val) => {
|
|
|
89
194
|
}
|
|
90
195
|
if (channelId) {
|
|
91
196
|
const res = await client.sendMessage(channelId, {
|
|
92
|
-
content:
|
|
197
|
+
content: finalContent,
|
|
93
198
|
type: 'image',
|
|
94
199
|
attachments: [fileAttachment]
|
|
95
200
|
});
|
|
@@ -97,7 +202,7 @@ const sendToRoom = async (client, param, val) => {
|
|
|
97
202
|
}
|
|
98
203
|
if (threadId) {
|
|
99
204
|
const res = await client.sendDm(threadId, {
|
|
100
|
-
content:
|
|
205
|
+
content: finalContent,
|
|
101
206
|
type: 'image',
|
|
102
207
|
attachments: [fileAttachment]
|
|
103
208
|
});
|
|
@@ -105,100 +210,13 @@ const sendToRoom = async (client, param, val) => {
|
|
|
105
210
|
}
|
|
106
211
|
return [createResult(ResultCode.Ok, '完成', null)];
|
|
107
212
|
}
|
|
108
|
-
|
|
109
|
-
if (mdAndButtons && mdAndButtons.length > 0) {
|
|
110
|
-
mdAndButtons.forEach(item => {
|
|
111
|
-
if (item.type === 'Markdown' && typeof item.value !== 'string') {
|
|
112
|
-
const md = item.value;
|
|
113
|
-
const map = {
|
|
114
|
-
'MD.title': value => `# ${value}`,
|
|
115
|
-
'MD.subtitle': value => `## ${value}`,
|
|
116
|
-
'MD.text': value => `${value} `,
|
|
117
|
-
'MD.bold': value => `**${value}** `,
|
|
118
|
-
'MD.divider': () => '\n————————\n',
|
|
119
|
-
'MD.italic': value => `_${value}_ `,
|
|
120
|
-
'MD.italicStar': value => `*${value}* `,
|
|
121
|
-
'MD.strikethrough': value => `~~${value}~~ `,
|
|
122
|
-
'MD.blockquote': value => `\n> ${value}`,
|
|
123
|
-
'MD.newline': () => '\n',
|
|
124
|
-
'MD.link': value => `[🔗${value.text}](${value.url}) `,
|
|
125
|
-
'MD.image': value => `\n\n`,
|
|
126
|
-
'MD.mention': (value, options) => {
|
|
127
|
-
const { belong } = options || {};
|
|
128
|
-
if (value === 'everyone' || value === 'all' || value === '' || typeof value !== 'string') {
|
|
129
|
-
return '<@everyone> ';
|
|
130
|
-
}
|
|
131
|
-
if (belong === 'user') {
|
|
132
|
-
return `<@${value}> `;
|
|
133
|
-
}
|
|
134
|
-
else if (belong === 'channel') {
|
|
135
|
-
return `<#${value}> `;
|
|
136
|
-
}
|
|
137
|
-
return '';
|
|
138
|
-
},
|
|
139
|
-
'MD.button': (value, options) => {
|
|
140
|
-
const autoEnter = options?.autoEnter ?? false;
|
|
141
|
-
const label = typeof value === 'object' ? value.title : value;
|
|
142
|
-
const command = options?.data || label;
|
|
143
|
-
return `<btn variant="borderless" command="${command}" enter="${String(autoEnter)}" >${label}</btn> `;
|
|
144
|
-
},
|
|
145
|
-
'MD.content': value => `${value}`
|
|
146
|
-
};
|
|
147
|
-
md.forEach(line => {
|
|
148
|
-
if (map[line.type]) {
|
|
149
|
-
const value = line?.value;
|
|
150
|
-
const options = line?.options;
|
|
151
|
-
contentMd += map[line.type](value, options);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
if (line.type === 'MD.list') {
|
|
155
|
-
const listStr = line.value.map(listItem => {
|
|
156
|
-
if (typeof listItem.value === 'object') {
|
|
157
|
-
return `\n${listItem.value.index}. ${listItem.value.text}`;
|
|
158
|
-
}
|
|
159
|
-
return `\n- ${listItem.value}`;
|
|
160
|
-
});
|
|
161
|
-
contentMd += `${listStr.join('')}\n`;
|
|
162
|
-
}
|
|
163
|
-
else if (line.type === 'MD.code') {
|
|
164
|
-
const language = line?.options?.language || '';
|
|
165
|
-
contentMd += `\n\`\`\`${language}\n${line.value}\n\`\`\`\n`;
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
const value = line['value'] || '';
|
|
169
|
-
contentMd += String(value);
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
else if (item.type === 'BT.group' && item.value.length > 0 && typeof item.value !== 'string') {
|
|
174
|
-
contentMd += `<box classWind="mt-2" variant="borderless" >${item.value
|
|
175
|
-
?.map(row => {
|
|
176
|
-
const val = row.value;
|
|
177
|
-
if (val.length === 0) {
|
|
178
|
-
return '';
|
|
179
|
-
}
|
|
180
|
-
return `<flex>${val
|
|
181
|
-
.map(button => {
|
|
182
|
-
const value = button?.value || {};
|
|
183
|
-
const options = button.options;
|
|
184
|
-
const autoEnter = options?.autoEnter ?? false;
|
|
185
|
-
const label = value;
|
|
186
|
-
const command = options?.data || label;
|
|
187
|
-
return `<btn command="${command}" enter="${String(autoEnter)}" >${label}</btn>`;
|
|
188
|
-
})
|
|
189
|
-
.join('')}</flex>`;
|
|
190
|
-
})
|
|
191
|
-
.join('')}</box>`;
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
if ((content && content.length > 0) || (contentMd && contentMd.length > 0)) {
|
|
213
|
+
if (finalContent && finalContent.length > 0) {
|
|
196
214
|
if (channelId) {
|
|
197
|
-
const res = await client.sendMessage(channelId, { content:
|
|
215
|
+
const res = await client.sendMessage(channelId, { content: finalContent, type: 'text' });
|
|
198
216
|
return [createResult(ResultCode.Ok, '完成', res)];
|
|
199
217
|
}
|
|
200
218
|
if (threadId) {
|
|
201
|
-
const res = await client.sendDm(threadId, { content:
|
|
219
|
+
const res = await client.sendDm(threadId, { content: finalContent, type: 'text' });
|
|
202
220
|
return [createResult(ResultCode.Ok, '完成', res)];
|
|
203
221
|
}
|
|
204
222
|
return [createResult(ResultCode.Ok, '完成', null)];
|