@aspalagin/openclaw-max 0.7.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/LICENSE +21 -0
- package/README.md +424 -0
- package/SKILL.md +108 -0
- package/dist/index.js +17 -0
- package/dist/setup-entry.js +8 -0
- package/dist/src/accounts.js +148 -0
- package/dist/src/actions.js +421 -0
- package/dist/src/api.js +553 -0
- package/dist/src/callbacks.js +111 -0
- package/dist/src/channel-agent-prompt.js +38 -0
- package/dist/src/channel-config.js +103 -0
- package/dist/src/channel-directory.js +84 -0
- package/dist/src/channel-lifecycle.js +219 -0
- package/dist/src/channel-outbound.js +133 -0
- package/dist/src/channel-policy.js +130 -0
- package/dist/src/channel.js +76 -0
- package/dist/src/config-schema.js +133 -0
- package/dist/src/deliver.js +126 -0
- package/dist/src/dispatch.js +186 -0
- package/dist/src/format.js +31 -0
- package/dist/src/inbound-attachments.js +125 -0
- package/dist/src/inbound.js +362 -0
- package/dist/src/media-temp.js +92 -0
- package/dist/src/model-buttons.js +96 -0
- package/dist/src/monitor-types.js +27 -0
- package/dist/src/monitor.js +40 -0
- package/dist/src/onboarding.js +180 -0
- package/dist/src/polling.js +98 -0
- package/dist/src/presentation.js +374 -0
- package/dist/src/runtime.js +36 -0
- package/dist/src/russian-trusted-ca.js +84 -0
- package/dist/src/send.js +612 -0
- package/dist/src/state.js +111 -0
- package/dist/src/sticker-cache.js +26 -0
- package/dist/src/stream-draft.js +102 -0
- package/dist/src/types.js +5 -0
- package/dist/src/webhook-queue.js +215 -0
- package/dist/src/webhook-runner.js +233 -0
- package/dist/src/webhook.js +196 -0
- package/openclaw.plugin.json +263 -0
- package/package.json +100 -0
- package/scripts/check-cycles.mjs +38 -0
- package/scripts/test-api.mjs +169 -0
- package/scripts/test-send.mjs +162 -0
- package/scripts/update-schema.mjs +48 -0
- package/src/accounts.ts +193 -0
- package/src/actions.ts +501 -0
- package/src/api.ts +758 -0
- package/src/callbacks.ts +140 -0
- package/src/channel-agent-prompt.ts +47 -0
- package/src/channel-config.ts +138 -0
- package/src/channel-directory.ts +90 -0
- package/src/channel-lifecycle.ts +256 -0
- package/src/channel-outbound.ts +159 -0
- package/src/channel-policy.ts +185 -0
- package/src/channel.ts +99 -0
- package/src/config-schema.ts +154 -0
- package/src/deliver.ts +168 -0
- package/src/dispatch.ts +224 -0
- package/src/format.ts +33 -0
- package/src/inbound-attachments.ts +152 -0
- package/src/inbound.ts +412 -0
- package/src/media-temp.ts +103 -0
- package/src/model-buttons.ts +150 -0
- package/src/monitor-types.ts +61 -0
- package/src/monitor.ts +49 -0
- package/src/onboarding.ts +215 -0
- package/src/polling.ts +125 -0
- package/src/presentation.ts +452 -0
- package/src/runtime.ts +56 -0
- package/src/russian-trusted-ca.ts +86 -0
- package/src/send.ts +774 -0
- package/src/state.ts +149 -0
- package/src/sticker-cache.ts +28 -0
- package/src/stream-draft.ts +125 -0
- package/src/types.ts +263 -0
- package/src/webhook-queue.ts +248 -0
- package/src/webhook-runner.ts +296 -0
- package/src/webhook.ts +268 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAX account resolution — reads config and produces a resolved account object.
|
|
3
|
+
*/
|
|
4
|
+
import { lstatSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from 'openclaw/plugin-sdk/core';
|
|
6
|
+
/**
|
|
7
|
+
* Get the MAX channel section from config.
|
|
8
|
+
*/
|
|
9
|
+
function getMaxSection(cfg) {
|
|
10
|
+
return cfg.channels?.max;
|
|
11
|
+
}
|
|
12
|
+
/** Read a secret from a regular (non-symlink) file; "" when missing or unreadable. */
|
|
13
|
+
export function readSecretFile(filePath) {
|
|
14
|
+
return readTokenFile(filePath);
|
|
15
|
+
}
|
|
16
|
+
function readTokenFile(tokenFile) {
|
|
17
|
+
const filePath = tokenFile?.trim();
|
|
18
|
+
if (!filePath)
|
|
19
|
+
return '';
|
|
20
|
+
try {
|
|
21
|
+
const stat = lstatSync(filePath);
|
|
22
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
23
|
+
return '';
|
|
24
|
+
return readFileSync(filePath, 'utf8').trim();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* List all MAX account IDs from config.
|
|
32
|
+
*/
|
|
33
|
+
export function listMaxAccountIds(cfg) {
|
|
34
|
+
const section = getMaxSection(cfg);
|
|
35
|
+
if (!section)
|
|
36
|
+
return [];
|
|
37
|
+
const ids = [];
|
|
38
|
+
// Check for default account (top-level botToken)
|
|
39
|
+
const hasDefault = section.botToken || section.tokenFile || process.env.MAX_BOT_TOKEN;
|
|
40
|
+
if (hasDefault)
|
|
41
|
+
ids.push(DEFAULT_ACCOUNT_ID);
|
|
42
|
+
// Check for named accounts
|
|
43
|
+
const accounts = section.accounts;
|
|
44
|
+
if (accounts) {
|
|
45
|
+
for (const key of Object.keys(accounts)) {
|
|
46
|
+
const normalized = normalizeAccountId(key);
|
|
47
|
+
if (normalized !== DEFAULT_ACCOUNT_ID && !ids.includes(normalized)) {
|
|
48
|
+
ids.push(normalized);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// If section exists but no token sources found, still return default
|
|
53
|
+
if (ids.length === 0 && section.enabled !== false) {
|
|
54
|
+
ids.push(DEFAULT_ACCOUNT_ID);
|
|
55
|
+
}
|
|
56
|
+
return ids;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the default account ID.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveDefaultMaxAccountId(_cfg) {
|
|
62
|
+
return DEFAULT_ACCOUNT_ID;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a single MAX account from config.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveMaxAccount(params) {
|
|
68
|
+
const { cfg, accountId: rawId } = params;
|
|
69
|
+
const accountId = rawId ? normalizeAccountId(rawId) : DEFAULT_ACCOUNT_ID;
|
|
70
|
+
const section = getMaxSection(cfg) ?? {};
|
|
71
|
+
const accounts = section.accounts;
|
|
72
|
+
let accountConfig;
|
|
73
|
+
let token = '';
|
|
74
|
+
let tokenSource = 'none';
|
|
75
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
76
|
+
// Default account: top-level config
|
|
77
|
+
accountConfig = {
|
|
78
|
+
enabled: section.enabled !== false,
|
|
79
|
+
botToken: section.botToken,
|
|
80
|
+
tokenFile: section.tokenFile,
|
|
81
|
+
name: section.name,
|
|
82
|
+
dmPolicy: section.dmPolicy,
|
|
83
|
+
allowFrom: section.allowFrom,
|
|
84
|
+
groups: section.groups,
|
|
85
|
+
groupPolicy: section.groupPolicy,
|
|
86
|
+
groupAllowFrom: section.groupAllowFrom,
|
|
87
|
+
transport: section.transport,
|
|
88
|
+
webhookUrl: section.webhookUrl,
|
|
89
|
+
webhookSecret: section.webhookSecret,
|
|
90
|
+
webhookSecretFile: section.webhookSecretFile,
|
|
91
|
+
webhookPath: section.webhookPath,
|
|
92
|
+
mediaMaxMb: section.mediaMaxMb,
|
|
93
|
+
streamMode: section.streamMode,
|
|
94
|
+
markSeen: section.markSeen,
|
|
95
|
+
};
|
|
96
|
+
if (accountConfig.botToken?.trim()) {
|
|
97
|
+
token = accountConfig.botToken.trim();
|
|
98
|
+
tokenSource = 'config';
|
|
99
|
+
}
|
|
100
|
+
else if (accountConfig.tokenFile?.trim()) {
|
|
101
|
+
token = readTokenFile(accountConfig.tokenFile);
|
|
102
|
+
tokenSource = token ? 'file' : 'none';
|
|
103
|
+
}
|
|
104
|
+
if (!token && process.env.MAX_BOT_TOKEN?.trim()) {
|
|
105
|
+
token = process.env.MAX_BOT_TOKEN.trim();
|
|
106
|
+
tokenSource = 'env';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// Named account
|
|
111
|
+
const raw = accounts?.[accountId] ?? {};
|
|
112
|
+
accountConfig = {
|
|
113
|
+
enabled: raw.enabled !== false,
|
|
114
|
+
botToken: raw.botToken,
|
|
115
|
+
tokenFile: raw.tokenFile,
|
|
116
|
+
name: raw.name,
|
|
117
|
+
dmPolicy: raw.dmPolicy,
|
|
118
|
+
allowFrom: raw.allowFrom,
|
|
119
|
+
groups: raw.groups,
|
|
120
|
+
groupPolicy: raw.groupPolicy,
|
|
121
|
+
groupAllowFrom: raw.groupAllowFrom,
|
|
122
|
+
transport: raw.transport,
|
|
123
|
+
webhookUrl: raw.webhookUrl,
|
|
124
|
+
webhookSecret: raw.webhookSecret,
|
|
125
|
+
webhookSecretFile: raw.webhookSecretFile,
|
|
126
|
+
webhookPath: raw.webhookPath,
|
|
127
|
+
mediaMaxMb: raw.mediaMaxMb,
|
|
128
|
+
streamMode: raw.streamMode,
|
|
129
|
+
markSeen: raw.markSeen,
|
|
130
|
+
};
|
|
131
|
+
if (accountConfig.botToken?.trim()) {
|
|
132
|
+
token = accountConfig.botToken.trim();
|
|
133
|
+
tokenSource = 'config';
|
|
134
|
+
}
|
|
135
|
+
else if (accountConfig.tokenFile?.trim()) {
|
|
136
|
+
token = readTokenFile(accountConfig.tokenFile);
|
|
137
|
+
tokenSource = token ? 'file' : 'none';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
accountId,
|
|
142
|
+
name: accountConfig.name,
|
|
143
|
+
enabled: accountConfig.enabled ?? true,
|
|
144
|
+
token,
|
|
145
|
+
tokenSource,
|
|
146
|
+
config: accountConfig,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAX channel message actions adapter — implements message tool actions
|
|
3
|
+
*/
|
|
4
|
+
import { normalizeMessagePresentation } from 'openclaw/plugin-sdk/interactive-runtime';
|
|
5
|
+
import { readStringParam } from 'openclaw/plugin-sdk/param-readers';
|
|
6
|
+
import { jsonResult } from 'openclaw/plugin-sdk/tool-results';
|
|
7
|
+
import { listMaxAccountIds, resolveMaxAccount } from './accounts.js';
|
|
8
|
+
import { materializeMaxPresentation, MAX_TEXT_LIMIT, readMaxDeliveryPin } from './presentation.js';
|
|
9
|
+
import { getMaxRuntime } from './runtime.js';
|
|
10
|
+
import { deleteMaxMessage, editMaxMessage, pinMaxMessage, readMaxChannelButtons, readMaxSendButtons, sendMaxContact, sendMaxLocation, sendMaxMediaGroup, sendMaxMediaMessage, sendMaxMessage, sendMaxSticker, unpinMaxMessage, } from './send.js';
|
|
11
|
+
import { getLastStickerCode } from './sticker-cache.js';
|
|
12
|
+
const providerId = 'max';
|
|
13
|
+
const mediaSourceKeys = ['media', 'filePath', 'path', 'fileUrl', 'url', 'buffer', 'image'];
|
|
14
|
+
function listEnabledAccounts(cfg) {
|
|
15
|
+
return listMaxAccountIds(cfg)
|
|
16
|
+
.map((accountId) => resolveMaxAccount({ cfg, accountId }))
|
|
17
|
+
.filter((account) => account.enabled && account.token);
|
|
18
|
+
}
|
|
19
|
+
function readTargetParam(params, required = true) {
|
|
20
|
+
return (readStringParam(params, 'to') ??
|
|
21
|
+
readStringParam(params, 'target') ??
|
|
22
|
+
readStringParam(params, 'chatId') ??
|
|
23
|
+
readStringParam(params, 'channelId', { required }));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Media sources in order: the direct media field first, then every
|
|
27
|
+
* structured `attachments[]` item (one source per item). Duplicates dropped.
|
|
28
|
+
*/
|
|
29
|
+
function readMediaSources(params) {
|
|
30
|
+
const sources = [];
|
|
31
|
+
const add = (value) => {
|
|
32
|
+
if (value && !sources.includes(value))
|
|
33
|
+
sources.push(value);
|
|
34
|
+
};
|
|
35
|
+
for (const key of mediaSourceKeys) {
|
|
36
|
+
const value = readStringParam(params, key, { trim: false });
|
|
37
|
+
if (value) {
|
|
38
|
+
add(value);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(params.attachments)) {
|
|
43
|
+
for (const item of params.attachments) {
|
|
44
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
45
|
+
continue;
|
|
46
|
+
const attachment = item;
|
|
47
|
+
for (const key of mediaSourceKeys) {
|
|
48
|
+
const value = typeof attachment[key] === 'string' ? attachment[key] : undefined;
|
|
49
|
+
if (value) {
|
|
50
|
+
add(value);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return sources;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Send one or more media sources: one goes as a single message, several as
|
|
60
|
+
* albums (up to 12 images/videos per message, audio/files one by one, order
|
|
61
|
+
* kept). Returns every sent message id; the first one carries the caption.
|
|
62
|
+
*/
|
|
63
|
+
async function sendMaxMediaSources(to, caption, sources, opts) {
|
|
64
|
+
if (sources.length === 1) {
|
|
65
|
+
const result = await sendMaxMediaMessage(to, caption, sources[0], opts);
|
|
66
|
+
return {
|
|
67
|
+
messageId: result.messageId,
|
|
68
|
+
messageIds: result.messageId ? [result.messageId] : [],
|
|
69
|
+
chatType: result.raw.message?.recipient?.chat_type,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// A failed group must not hide the ones already sent (a retry by the agent
|
|
73
|
+
// would duplicate them): report partial failures, throw only when none went.
|
|
74
|
+
const errors = [];
|
|
75
|
+
let firstError;
|
|
76
|
+
const { messageIds } = await sendMaxMediaGroup(to, caption, sources, {
|
|
77
|
+
...opts,
|
|
78
|
+
onError: (err, failed) => {
|
|
79
|
+
firstError ??= err;
|
|
80
|
+
errors.push(`${failed.join(', ')}: ${String(err)}`);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
if (messageIds.length === 0 && firstError !== undefined)
|
|
84
|
+
throw firstError;
|
|
85
|
+
return {
|
|
86
|
+
messageId: messageIds[0] ?? '',
|
|
87
|
+
messageIds,
|
|
88
|
+
...(errors.length ? { mediaErrors: errors } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** messageIds / mediaErrors for a multi-message media send. */
|
|
92
|
+
function mediaResultExtra(result) {
|
|
93
|
+
return {
|
|
94
|
+
...(result.messageIds.length > 1 ? { messageIds: result.messageIds } : {}),
|
|
95
|
+
...(result.mediaErrors ? { mediaErrors: result.mediaErrors } : {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Coordinates from `latitude`+`longitude` (both required) or a
|
|
100
|
+
* `location: "LAT,LNG"` / "LAT LNG" string; undefined when absent or not
|
|
101
|
+
* numeric.
|
|
102
|
+
*/
|
|
103
|
+
function readLocationParams(params) {
|
|
104
|
+
let lat;
|
|
105
|
+
let lng;
|
|
106
|
+
if (params.latitude != null && params.longitude != null) {
|
|
107
|
+
lat = parseFloat(String(params.latitude));
|
|
108
|
+
lng = parseFloat(String(params.longitude));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const locationStr = readStringParam(params, 'location');
|
|
112
|
+
const m = locationStr?.match(/(-?\d+\.?\d*)[,\s]+(-?\d+\.?\d*)/);
|
|
113
|
+
if (m) {
|
|
114
|
+
lat = parseFloat(m[1]);
|
|
115
|
+
lng = parseFloat(m[2]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (lat == null || lng == null || isNaN(lat) || isNaN(lng))
|
|
119
|
+
return undefined;
|
|
120
|
+
return { latitude: lat, longitude: lng };
|
|
121
|
+
}
|
|
122
|
+
/** Contact card fields: contactId (MAX user id) and vcfPhone/phone next to the given name. */
|
|
123
|
+
function readContactParams(params, name) {
|
|
124
|
+
const contactId = params.contactId != null ? Number(params.contactId) : undefined;
|
|
125
|
+
const vcfPhone = readStringParam(params, 'vcfPhone') ?? readStringParam(params, 'phone');
|
|
126
|
+
return {
|
|
127
|
+
name,
|
|
128
|
+
contactId: contactId && !isNaN(contactId) ? contactId : undefined,
|
|
129
|
+
vcfPhone: vcfPhone ?? undefined,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export const maxMessageActions = {
|
|
133
|
+
describeMessageTool: ({ cfg }) => {
|
|
134
|
+
const accounts = listEnabledAccounts(cfg);
|
|
135
|
+
if (accounts.length === 0) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
actions: ['send', 'edit', 'delete', 'sticker', 'sendAttachment', 'pin', 'unpin'],
|
|
140
|
+
// presentation → inline keyboard + MAX markdown (presentation.ts);
|
|
141
|
+
// delivery-pin → PUT /chats/{chatId}/pin on the sent message.
|
|
142
|
+
capabilities: ['presentation', 'delivery-pin'],
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
extractToolSend: ({ args }) => {
|
|
146
|
+
// Extract routing info for ALL actions (send, edit, delete, sticker)
|
|
147
|
+
// Core uses extractToolSend for routing all message tool actions to plugin
|
|
148
|
+
let to = typeof args.target === 'string'
|
|
149
|
+
? args.target
|
|
150
|
+
: typeof args.to === 'string'
|
|
151
|
+
? args.to
|
|
152
|
+
: typeof args.chatId === 'string'
|
|
153
|
+
? args.chatId
|
|
154
|
+
: typeof args.channelId === 'string'
|
|
155
|
+
? args.channelId
|
|
156
|
+
: undefined;
|
|
157
|
+
if (!to) {
|
|
158
|
+
// For edit/delete, target may not be present — use a placeholder
|
|
159
|
+
// so core still routes to this plugin's handleAction
|
|
160
|
+
to = typeof args.messageId === 'string' ? '__message_action__' : undefined;
|
|
161
|
+
}
|
|
162
|
+
if (!to) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
// Strip provider prefix (e.g. "max:188862440" → "188862440")
|
|
166
|
+
if (to.startsWith('max:'))
|
|
167
|
+
to = to.slice(4);
|
|
168
|
+
const accountId = typeof args.accountId === 'string' ? args.accountId.trim() : undefined;
|
|
169
|
+
return { to, accountId };
|
|
170
|
+
},
|
|
171
|
+
handleAction: async ({ action, params, cfg, accountId }) => {
|
|
172
|
+
const account = resolveMaxAccount({
|
|
173
|
+
cfg,
|
|
174
|
+
accountId,
|
|
175
|
+
});
|
|
176
|
+
if (!account.token) {
|
|
177
|
+
throw new Error('MAX bot token not configured');
|
|
178
|
+
}
|
|
179
|
+
// Strip provider prefix from target (e.g. "max:188862440" → "188862440")
|
|
180
|
+
const stripPrefix = (val) => {
|
|
181
|
+
if (!val)
|
|
182
|
+
return val;
|
|
183
|
+
return val.startsWith('max:') ? val.slice(4) : val;
|
|
184
|
+
};
|
|
185
|
+
if (action === 'send') {
|
|
186
|
+
const to = stripPrefix(readTargetParam(params));
|
|
187
|
+
const presentation = normalizeMessagePresentation(params.presentation);
|
|
188
|
+
const pin = readMaxDeliveryPin(params.delivery, params.pin);
|
|
189
|
+
const content = readStringParam(params, 'message', {
|
|
190
|
+
required: !presentation,
|
|
191
|
+
allowEmpty: true,
|
|
192
|
+
}) ?? '';
|
|
193
|
+
// Pin the sent message when delivery.pin (or pin=true) was requested.
|
|
194
|
+
// Optional pin failures degrade; a required one fails the action.
|
|
195
|
+
// Dialogs cannot pin: that is reported as pinned:false with pinSkipped.
|
|
196
|
+
const withPin = async (messageId, chatType, extra = {}) => {
|
|
197
|
+
if (!pin || !messageId)
|
|
198
|
+
return jsonResult({ ok: true, to, messageId, ...extra });
|
|
199
|
+
try {
|
|
200
|
+
const pinned = await pinMaxMessage(to, messageId, {
|
|
201
|
+
token: account.token,
|
|
202
|
+
pinNotify: pin.notify === true,
|
|
203
|
+
chatType,
|
|
204
|
+
});
|
|
205
|
+
if (!pinned.pinned) {
|
|
206
|
+
return jsonResult({
|
|
207
|
+
ok: true,
|
|
208
|
+
to,
|
|
209
|
+
messageId,
|
|
210
|
+
...extra,
|
|
211
|
+
pinned: false,
|
|
212
|
+
pinSkipped: pinned.reason,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return jsonResult({ ok: true, to, messageId, ...extra, pinned: true });
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
if (pin.required)
|
|
219
|
+
throw err;
|
|
220
|
+
return jsonResult({
|
|
221
|
+
ok: true,
|
|
222
|
+
to,
|
|
223
|
+
messageId,
|
|
224
|
+
...extra,
|
|
225
|
+
pinned: false,
|
|
226
|
+
pinError: String(err),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
// Core normally renders presentation through the outbound adapter; this
|
|
231
|
+
// path covers direct plugin dispatch with the same render policy.
|
|
232
|
+
if (presentation) {
|
|
233
|
+
const rendered = await materializeMaxPresentation({ text: content, presentation });
|
|
234
|
+
const renderedButtons = readMaxChannelButtons(rendered.channelData);
|
|
235
|
+
const text = rendered.text ?? '';
|
|
236
|
+
const chunks = text.length > MAX_TEXT_LIMIT
|
|
237
|
+
? getMaxRuntime().channel.text.chunkMarkdownText(text, MAX_TEXT_LIMIT)
|
|
238
|
+
: [text];
|
|
239
|
+
let firstMessageId = '';
|
|
240
|
+
let firstChatType;
|
|
241
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
242
|
+
const sent = await sendMaxMessage(to, chunks[index], {
|
|
243
|
+
token: account.token,
|
|
244
|
+
replyToMessageId: index === 0 ? (readStringParam(params, 'replyTo') ?? undefined) : undefined,
|
|
245
|
+
format: 'markdown',
|
|
246
|
+
buttons: index === chunks.length - 1 ? renderedButtons : undefined,
|
|
247
|
+
});
|
|
248
|
+
if (index === 0) {
|
|
249
|
+
firstMessageId = sent.messageId;
|
|
250
|
+
firstChatType = sent.raw.message?.recipient?.chat_type;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return withPin(firstMessageId, firstChatType);
|
|
254
|
+
}
|
|
255
|
+
const replyTo = readStringParam(params, 'replyTo');
|
|
256
|
+
const stickerId = readStringParam(params, 'stickerId');
|
|
257
|
+
// Inline keyboard buttons: [[{text, type?, payload?|callback_data?, url?, webApp?}]]
|
|
258
|
+
// type: callback (default) | link | message | clipboard | open_app | request_contact | request_geo_location
|
|
259
|
+
const buttons = readMaxSendButtons(params.buttons);
|
|
260
|
+
// Sticker sending (by sticker code)
|
|
261
|
+
if (stickerId) {
|
|
262
|
+
// stickerId can be a single id or comma-separated
|
|
263
|
+
const codes = Array.isArray(params.stickerId)
|
|
264
|
+
? params.stickerId
|
|
265
|
+
: [stickerId];
|
|
266
|
+
const firstCode = codes[0];
|
|
267
|
+
if (firstCode) {
|
|
268
|
+
const result = await sendMaxSticker(to, firstCode, {
|
|
269
|
+
token: account.token,
|
|
270
|
+
replyToMessageId: replyTo ?? undefined,
|
|
271
|
+
});
|
|
272
|
+
return withPin(result.messageId, result.raw.message?.recipient?.chat_type);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Location sending: coordinates from latitude/longitude or location="LAT,LNG"
|
|
276
|
+
const location = readLocationParams(params);
|
|
277
|
+
if (location) {
|
|
278
|
+
const result = await sendMaxLocation(to, location, content || undefined, {
|
|
279
|
+
token: account.token,
|
|
280
|
+
replyToMessageId: replyTo ?? undefined,
|
|
281
|
+
format: 'markdown',
|
|
282
|
+
});
|
|
283
|
+
return withPin(result.messageId, result.raw.message?.recipient?.chat_type);
|
|
284
|
+
}
|
|
285
|
+
// Contact sending: if contactName param exists
|
|
286
|
+
const contactName = readStringParam(params, 'contactName');
|
|
287
|
+
if (contactName) {
|
|
288
|
+
const result = await sendMaxContact(to, readContactParams(params, contactName), {
|
|
289
|
+
token: account.token,
|
|
290
|
+
replyToMessageId: replyTo ?? undefined,
|
|
291
|
+
});
|
|
292
|
+
return withPin(result.messageId, result.raw.message?.recipient?.chat_type);
|
|
293
|
+
}
|
|
294
|
+
// Media: direct media fields and every structured attachments[] item.
|
|
295
|
+
const mediaSources = readMediaSources(params);
|
|
296
|
+
if (mediaSources.length) {
|
|
297
|
+
// Local path or URL: https image links go by URL, other remote media
|
|
298
|
+
// is downloaded to a temp file and uploaded.
|
|
299
|
+
const result = await sendMaxMediaSources(to, content, mediaSources, {
|
|
300
|
+
token: account.token,
|
|
301
|
+
replyToMessageId: replyTo ?? undefined,
|
|
302
|
+
format: 'markdown',
|
|
303
|
+
mediaMaxBytes: (account.config.mediaMaxMb ?? 20) * 1024 * 1024,
|
|
304
|
+
});
|
|
305
|
+
return withPin(result.messageId, result.chatType, mediaResultExtra(result));
|
|
306
|
+
}
|
|
307
|
+
const result = await sendMaxMessage(to, content, {
|
|
308
|
+
token: account.token,
|
|
309
|
+
replyToMessageId: replyTo ?? undefined,
|
|
310
|
+
format: 'markdown',
|
|
311
|
+
buttons,
|
|
312
|
+
});
|
|
313
|
+
return withPin(result.messageId, result.raw.message?.recipient?.chat_type);
|
|
314
|
+
}
|
|
315
|
+
if (action === 'edit') {
|
|
316
|
+
const messageId = readStringParam(params, 'messageId', { required: true });
|
|
317
|
+
const text = readStringParam(params, 'message', {
|
|
318
|
+
required: true,
|
|
319
|
+
allowEmpty: true,
|
|
320
|
+
});
|
|
321
|
+
await editMaxMessage(messageId, text, {
|
|
322
|
+
token: account.token,
|
|
323
|
+
format: 'markdown',
|
|
324
|
+
});
|
|
325
|
+
return jsonResult({ ok: true, messageId });
|
|
326
|
+
}
|
|
327
|
+
if (action === 'delete') {
|
|
328
|
+
const messageId = readStringParam(params, 'messageId', { required: true });
|
|
329
|
+
await deleteMaxMessage(messageId, {
|
|
330
|
+
token: account.token,
|
|
331
|
+
});
|
|
332
|
+
return jsonResult({ ok: true, messageId });
|
|
333
|
+
}
|
|
334
|
+
if (action === 'pin') {
|
|
335
|
+
const to = stripPrefix(readTargetParam(params));
|
|
336
|
+
const messageId = readStringParam(params, 'messageId', { required: true });
|
|
337
|
+
const notifyParam = params.notify;
|
|
338
|
+
const pinned = await pinMaxMessage(to, messageId, {
|
|
339
|
+
token: account.token,
|
|
340
|
+
pinNotify: typeof notifyParam === 'boolean' ? notifyParam : undefined,
|
|
341
|
+
});
|
|
342
|
+
if (!pinned.pinned) {
|
|
343
|
+
return jsonResult({ ok: true, to, messageId, pinned: false, reason: pinned.reason });
|
|
344
|
+
}
|
|
345
|
+
return jsonResult({ ok: true, to, messageId, pinned: true });
|
|
346
|
+
}
|
|
347
|
+
if (action === 'unpin') {
|
|
348
|
+
const to = stripPrefix(readTargetParam(params));
|
|
349
|
+
await unpinMaxMessage(to, { token: account.token });
|
|
350
|
+
return jsonResult({ ok: true, to, pinned: false });
|
|
351
|
+
}
|
|
352
|
+
if (action === 'sticker') {
|
|
353
|
+
const to = stripPrefix(readTargetParam(params));
|
|
354
|
+
// stickerId may come as string or string[] from message tool schema
|
|
355
|
+
const rawStickerId = params.stickerId;
|
|
356
|
+
let stickerCode = Array.isArray(rawStickerId)
|
|
357
|
+
? rawStickerId[0]?.trim()
|
|
358
|
+
: (readStringParam(params, 'stickerId') ?? readStringParam(params, 'fileId'));
|
|
359
|
+
// Auto-fill from last received sticker if not provided
|
|
360
|
+
if (!stickerCode) {
|
|
361
|
+
stickerCode = getLastStickerCode(to) ?? getLastStickerCode() ?? undefined;
|
|
362
|
+
}
|
|
363
|
+
if (!stickerCode) {
|
|
364
|
+
throw new Error('stickerId is required. Send a sticker first, then ask to send it back.');
|
|
365
|
+
}
|
|
366
|
+
const replyTo = readStringParam(params, 'replyTo');
|
|
367
|
+
const result = await sendMaxSticker(to, stickerCode, {
|
|
368
|
+
token: account.token,
|
|
369
|
+
replyToMessageId: replyTo ?? undefined,
|
|
370
|
+
});
|
|
371
|
+
return jsonResult({ ok: true, to, messageId: result.messageId });
|
|
372
|
+
}
|
|
373
|
+
if (action === 'sendAttachment') {
|
|
374
|
+
const to = stripPrefix(readTargetParam(params));
|
|
375
|
+
const replyTo = readStringParam(params, 'replyTo');
|
|
376
|
+
const caption = readStringParam(params, 'message') ?? readStringParam(params, 'caption') ?? '';
|
|
377
|
+
const attachType = readStringParam(params, 'type') ?? readStringParam(params, 'attachmentType') ?? '';
|
|
378
|
+
const mediaSources = readMediaSources(params);
|
|
379
|
+
if (mediaSources.length) {
|
|
380
|
+
const result = await sendMaxMediaSources(to, caption, mediaSources, {
|
|
381
|
+
token: account.token,
|
|
382
|
+
replyToMessageId: replyTo ?? undefined,
|
|
383
|
+
format: 'markdown',
|
|
384
|
+
mediaMaxBytes: (account.config.mediaMaxMb ?? 20) * 1024 * 1024,
|
|
385
|
+
});
|
|
386
|
+
return jsonResult({
|
|
387
|
+
ok: true,
|
|
388
|
+
to,
|
|
389
|
+
messageId: result.messageId,
|
|
390
|
+
...mediaResultExtra(result),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
// Location attachment
|
|
394
|
+
if (attachType === 'location' ||
|
|
395
|
+
params.latitude != null ||
|
|
396
|
+
params.longitude != null ||
|
|
397
|
+
readStringParam(params, 'location')) {
|
|
398
|
+
const location = readLocationParams(params);
|
|
399
|
+
if (location) {
|
|
400
|
+
const result = await sendMaxLocation(to, location, caption || undefined, {
|
|
401
|
+
token: account.token,
|
|
402
|
+
replyToMessageId: replyTo ?? undefined,
|
|
403
|
+
});
|
|
404
|
+
return jsonResult({ ok: true, to, messageId: result.messageId });
|
|
405
|
+
}
|
|
406
|
+
throw new Error("Invalid location: provide latitude/longitude or location='LAT,LNG'");
|
|
407
|
+
}
|
|
408
|
+
// Contact attachment
|
|
409
|
+
if (attachType === 'contact' || readStringParam(params, 'contactName')) {
|
|
410
|
+
const contactName = readStringParam(params, 'contactName') ?? readStringParam(params, 'name') ?? 'Unknown';
|
|
411
|
+
const result = await sendMaxContact(to, readContactParams(params, contactName), {
|
|
412
|
+
token: account.token,
|
|
413
|
+
replyToMessageId: replyTo ?? undefined,
|
|
414
|
+
});
|
|
415
|
+
return jsonResult({ ok: true, to, messageId: result.messageId });
|
|
416
|
+
}
|
|
417
|
+
throw new Error("sendAttachment: unknown type. Use media/filePath for files, or type='location' / type='contact'");
|
|
418
|
+
}
|
|
419
|
+
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
|
|
420
|
+
},
|
|
421
|
+
};
|