@kartik.doda/openclaw-plugin-test 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/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/cli/admin-api.d.ts +58 -0
- package/dist/cli/admin-api.d.ts.map +1 -0
- package/dist/cli/admin-api.js +242 -0
- package/dist/cli/admin-api.js.map +1 -0
- package/dist/cli/auth.d.ts +5 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +184 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/bot.d.ts +3 -0
- package/dist/cli/bot.d.ts.map +1 -0
- package/dist/cli/bot.js +100 -0
- package/dist/cli/bot.js.map +1 -0
- package/dist/cli/config-updater.d.ts +70 -0
- package/dist/cli/config-updater.d.ts.map +1 -0
- package/dist/cli/config-updater.js +277 -0
- package/dist/cli/config-updater.js.map +1 -0
- package/dist/cli/credentials.d.ts +14 -0
- package/dist/cli/credentials.d.ts.map +1 -0
- package/dist/cli/credentials.js +45 -0
- package/dist/cli/credentials.js.map +1 -0
- package/dist/cli/rate-limiter.d.ts +17 -0
- package/dist/cli/rate-limiter.d.ts.map +1 -0
- package/dist/cli/rate-limiter.js +78 -0
- package/dist/cli/rate-limiter.js.map +1 -0
- package/dist/cli/setup.d.ts +2 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +292 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/ui.d.ts +30 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +202 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/client/ddp.d.ts +31 -0
- package/dist/client/ddp.d.ts.map +1 -0
- package/dist/client/ddp.js +86 -0
- package/dist/client/ddp.js.map +1 -0
- package/dist/client/rest.d.ts +49 -0
- package/dist/client/rest.d.ts.map +1 -0
- package/dist/client/rest.js +347 -0
- package/dist/client/rest.js.map +1 -0
- package/dist/config/access-store.d.ts +29 -0
- package/dist/config/access-store.d.ts.map +1 -0
- package/dist/config/access-store.js +71 -0
- package/dist/config/access-store.js.map +1 -0
- package/dist/config/schema.d.ts +33 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +52 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/store.d.ts +11 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +82 -0
- package/dist/config/store.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +164 -0
- package/dist/plugin.js.map +1 -0
- package/dist/service/attachments.d.ts +10 -0
- package/dist/service/attachments.d.ts.map +1 -0
- package/dist/service/attachments.js +105 -0
- package/dist/service/attachments.js.map +1 -0
- package/dist/service/channel.d.ts +25 -0
- package/dist/service/channel.d.ts.map +1 -0
- package/dist/service/channel.js +747 -0
- package/dist/service/channel.js.map +1 -0
- package/dist/service/gateway.d.ts +7 -0
- package/dist/service/gateway.d.ts.map +1 -0
- package/dist/service/gateway.js +621 -0
- package/dist/service/gateway.js.map +1 -0
- package/dist/service/group-history.d.ts +8 -0
- package/dist/service/group-history.d.ts.map +1 -0
- package/dist/service/group-history.js +24 -0
- package/dist/service/group-history.js.map +1 -0
- package/dist/service/inbound.d.ts +16 -0
- package/dist/service/inbound.d.ts.map +1 -0
- package/dist/service/inbound.js +189 -0
- package/dist/service/inbound.js.map +1 -0
- package/dist/service/runtime-state.d.ts +8 -0
- package/dist/service/runtime-state.d.ts.map +1 -0
- package/dist/service/runtime-state.js +3 -0
- package/dist/service/runtime-state.js.map +1 -0
- package/dist/types.d.ts +220 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +15 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +68 -0
- package/dist/utils.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import { DM_SCOPE } from "../utils.js";
|
|
2
|
+
import { RocketChatClient } from "../client/rest.js";
|
|
3
|
+
import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { readAllAccounts, readBindingsForAccount, readOwner, readAccount, addAccount, addBinding, ensureAgentForBot, removeBindingsForAccount, removeAccount, removeAgentDir, } from "../cli/config-updater.js";
|
|
7
|
+
import { createBotUser, loginAs, getGroupByName, createDirectMessage, sendMessage, deleteUser, getUserInfo, inviteToGroup, listGroupMembers, } from "../cli/admin-api.js";
|
|
8
|
+
import { checkBotCreationLimit, recordBotCreation } from "../cli/rate-limiter.js";
|
|
9
|
+
import { loadAdmin } from "../cli/credentials.js";
|
|
10
|
+
import { startGateway } from "./gateway.js";
|
|
11
|
+
import { activeClients, connectionStatus } from "./runtime-state.js";
|
|
12
|
+
import { AccessStore } from "../config/access-store.js";
|
|
13
|
+
const BROADCAST_MENTIONS = new Set(["here", "all", "everyone"]);
|
|
14
|
+
export function shouldHandleInboundEvent(event, options) {
|
|
15
|
+
if (event.senderId === options.botUserId) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (options.knownBotUserIds?.has(event.senderId)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (event.roomType === "direct") {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
const aliases = new Set(options.mentionNames.map((a) => a.trim().replace(/^@+/, "").toLowerCase()));
|
|
25
|
+
for (const mention of event.mentions) {
|
|
26
|
+
const name = mention.toLowerCase();
|
|
27
|
+
if (BROADCAST_MENTIONS.has(name))
|
|
28
|
+
continue;
|
|
29
|
+
if (aliases.has(name))
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
const normalizedText = event.text.toLowerCase();
|
|
33
|
+
for (const alias of aliases) {
|
|
34
|
+
if (normalizedText.includes(`@${alias}`))
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const COMMAND_RE = /^\s*!(\S+)(?:\s+([\s\S]*))?$/i;
|
|
40
|
+
/**
|
|
41
|
+
* Commands restricted to the bot owner (`accounts.<id>.owner`). Any other user
|
|
42
|
+
* with access who tries one of these gets a permission reply and the command is
|
|
43
|
+
* never executed or forwarded to OpenClaw.
|
|
44
|
+
*/
|
|
45
|
+
const OWNER_ONLY_COMMANDS = new Set([
|
|
46
|
+
"add-bot",
|
|
47
|
+
"remove-bot",
|
|
48
|
+
"add-group",
|
|
49
|
+
"revoke",
|
|
50
|
+
"access",
|
|
51
|
+
"bots",
|
|
52
|
+
]);
|
|
53
|
+
function isOwner(ctx) {
|
|
54
|
+
const owner = ctx.account.owner?.trim().replace(/^@+/, "").toLowerCase();
|
|
55
|
+
const actor = ctx.senderName?.trim().replace(/^@+/, "").toLowerCase();
|
|
56
|
+
return !!owner && !!actor && owner === actor;
|
|
57
|
+
}
|
|
58
|
+
export async function matchCommand(text, ctx) {
|
|
59
|
+
const normalized = text
|
|
60
|
+
.replace(/[\u200B\u200C\u200D\uFEFF]/g, "")
|
|
61
|
+
.replace(/^\s*(@\S+\s+)+/, "")
|
|
62
|
+
.trim()
|
|
63
|
+
.replace(/^!\s+/, "!");
|
|
64
|
+
const match = normalized.match(COMMAND_RE);
|
|
65
|
+
if (!match)
|
|
66
|
+
return { action: "passthrough" };
|
|
67
|
+
const cmd = match[1].toLowerCase();
|
|
68
|
+
const argStr = match[2] ?? "";
|
|
69
|
+
if (OWNER_ONLY_COMMANDS.has(cmd) && !isOwner(ctx)) {
|
|
70
|
+
return {
|
|
71
|
+
action: "reply",
|
|
72
|
+
replyText: `\`!${cmd}\` is owner-only. Contact ${ctx.account.owner ? `@${ctx.account.owner}` : "the bot owner"}.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
switch (cmd) {
|
|
76
|
+
case "help":
|
|
77
|
+
return { action: "reply", replyText: buildHelpText(isOwner(ctx)) };
|
|
78
|
+
case "access":
|
|
79
|
+
return { action: "reply", replyText: await runAccess(ctx) };
|
|
80
|
+
case "bots":
|
|
81
|
+
return { action: "reply", replyText: runBots() };
|
|
82
|
+
case "groups":
|
|
83
|
+
return { action: "reply", replyText: await runGroups(ctx) };
|
|
84
|
+
case "status":
|
|
85
|
+
return { action: "reply", replyText: await runStatus(ctx) };
|
|
86
|
+
case "add-bot":
|
|
87
|
+
return { action: "reply", replyText: await runAddBot(ctx, argStr) };
|
|
88
|
+
case "lend":
|
|
89
|
+
return { action: "reply", replyText: await runLend(ctx, argStr) };
|
|
90
|
+
case "revoke":
|
|
91
|
+
return { action: "reply", replyText: await runRevoke(ctx, argStr) };
|
|
92
|
+
case "remove-bot":
|
|
93
|
+
return { action: "reply", replyText: await runRemoveBot(ctx, argStr) };
|
|
94
|
+
case "add-group":
|
|
95
|
+
return { action: "reply", replyText: await runAddGroup(ctx, argStr) };
|
|
96
|
+
case "compact":
|
|
97
|
+
return { action: "openclaw-command", command: `/compact${argStr ? " " + argStr : ""}` };
|
|
98
|
+
case "reset":
|
|
99
|
+
return { action: "openclaw-command", command: `/reset${argStr ? " " + argStr : ""}` };
|
|
100
|
+
case "new":
|
|
101
|
+
return { action: "openclaw-command", command: `/new${argStr ? " " + argStr : ""}` };
|
|
102
|
+
case "model":
|
|
103
|
+
return { action: "openclaw-command", command: `/model${argStr ? " " + argStr : ""}` };
|
|
104
|
+
case "tools":
|
|
105
|
+
return { action: "openclaw-command", command: `/tools${argStr ? " " + argStr : ""}` };
|
|
106
|
+
case "skill":
|
|
107
|
+
return { action: "openclaw-command", command: `/skill${argStr ? " " + argStr : ""}` };
|
|
108
|
+
case "skills":
|
|
109
|
+
return { action: "reply", replyText: runSkills() };
|
|
110
|
+
case "think":
|
|
111
|
+
return { action: "openclaw-command", command: `/think${argStr ? " " + argStr : ""}` };
|
|
112
|
+
case "abort":
|
|
113
|
+
return { action: "openclaw-command", command: `/abort${argStr ? " " + argStr : ""}` };
|
|
114
|
+
case "reasoning":
|
|
115
|
+
return { action: "openclaw-command", command: `/reasoning${argStr ? " " + argStr : ""}` };
|
|
116
|
+
case "verbose":
|
|
117
|
+
return { action: "openclaw-command", command: `/verbose${argStr ? " " + argStr : ""}` };
|
|
118
|
+
default:
|
|
119
|
+
return {
|
|
120
|
+
action: "reply",
|
|
121
|
+
replyText: `Unknown command \`!${cmd}\`. Type \`!help\` to see available commands.`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function buildHelpText(showAll) {
|
|
126
|
+
const groups = [
|
|
127
|
+
[
|
|
128
|
+
"Bot",
|
|
129
|
+
[
|
|
130
|
+
["help", "this menu"],
|
|
131
|
+
["status", "Checkout openclaw gateway status"],
|
|
132
|
+
["bots", "bots and agents"],
|
|
133
|
+
["groups", "groups joined by bots"],
|
|
134
|
+
["access", "who can use"],
|
|
135
|
+
["add-bot <user>", "create a bot"],
|
|
136
|
+
["remove-bot <user...>", "delete bot(s)"],
|
|
137
|
+
["add-group <group> [bot]", "invite bot to group"],
|
|
138
|
+
["lend <group> <user>", "grant group access"],
|
|
139
|
+
["lend dm <user>", "grant DM access"],
|
|
140
|
+
["revoke <group> <user>", "revoke group access"],
|
|
141
|
+
["revoke dm <user>", "revoke DM access"],
|
|
142
|
+
],
|
|
143
|
+
],
|
|
144
|
+
[
|
|
145
|
+
"Context",
|
|
146
|
+
[
|
|
147
|
+
["compact", "compress history"],
|
|
148
|
+
["reset", "wipe all"],
|
|
149
|
+
["new [model]", "fresh start"],
|
|
150
|
+
],
|
|
151
|
+
],
|
|
152
|
+
["Model", [["model [name|#]", "list or switch"]]],
|
|
153
|
+
[
|
|
154
|
+
"Behavior",
|
|
155
|
+
[
|
|
156
|
+
["think <level>", "off, low, medium, high"],
|
|
157
|
+
["abort", "stop reply"],
|
|
158
|
+
["reasoning on/off", "show reasoning"],
|
|
159
|
+
["verbose on/off", "debug details"],
|
|
160
|
+
],
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
"Tools & Skills",
|
|
164
|
+
[
|
|
165
|
+
["tools", "list agent tools"],
|
|
166
|
+
["skills", "installed skills"],
|
|
167
|
+
["skill <name>", "run a skill"],
|
|
168
|
+
],
|
|
169
|
+
],
|
|
170
|
+
];
|
|
171
|
+
const lines = ["Commands"];
|
|
172
|
+
for (const [title, cmds] of groups) {
|
|
173
|
+
const visible = showAll
|
|
174
|
+
? cmds
|
|
175
|
+
: cmds.filter(([cmd]) => !OWNER_ONLY_COMMANDS.has(cmd.split(/\s/)[0]));
|
|
176
|
+
if (visible.length === 0)
|
|
177
|
+
continue;
|
|
178
|
+
lines.push("", `**${title}**`);
|
|
179
|
+
for (const [cmd, desc] of visible) {
|
|
180
|
+
lines.push(`\`!${cmd}\` ${desc}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return lines.join("\n");
|
|
184
|
+
}
|
|
185
|
+
function runBots() {
|
|
186
|
+
const accounts = readAllAccounts();
|
|
187
|
+
if (accounts.length === 0) {
|
|
188
|
+
return "No Rocket.Chat bot accounts configured.";
|
|
189
|
+
}
|
|
190
|
+
const lines = [];
|
|
191
|
+
for (const account of accounts) {
|
|
192
|
+
const mention = account.mentionNames[0] ?? account.accountId;
|
|
193
|
+
const bindings = readBindingsForAccount(account.accountId);
|
|
194
|
+
if (bindings.length === 0) {
|
|
195
|
+
lines.push(`- ${mention} - (no agent bound)`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
for (const binding of bindings) {
|
|
199
|
+
const scope = binding.peer ? `${binding.peer.kind} ${binding.peer.id}` : "global";
|
|
200
|
+
lines.push(`- ${mention} → ${binding.agentId} (${scope})`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return ["**Bot accounts**", ...lines].join("\n");
|
|
204
|
+
}
|
|
205
|
+
function parseSkillFrontmatter(content) {
|
|
206
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
207
|
+
if (!fmMatch)
|
|
208
|
+
return {};
|
|
209
|
+
const fm = fmMatch[1];
|
|
210
|
+
const result = {};
|
|
211
|
+
const nameLine = fm.match(/^name:\s*(.+)$/m);
|
|
212
|
+
if (nameLine)
|
|
213
|
+
result.name = nameLine[1].trim().replace(/^["']|["']$/g, "");
|
|
214
|
+
const descLine = fm.match(/^description:\s*(.+)$/m);
|
|
215
|
+
if (descLine)
|
|
216
|
+
result.description = descLine[1].trim().replace(/^["']|["']$/g, "");
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
function runSkills() {
|
|
220
|
+
const skillsDir = resolve(homedir(), ".openclaw", "skills");
|
|
221
|
+
if (!existsSync(skillsDir)) {
|
|
222
|
+
return "No skills installed (expected at ~/.openclaw/skills).";
|
|
223
|
+
}
|
|
224
|
+
const entries = readdirSync(skillsDir).filter((name) => {
|
|
225
|
+
const full = resolve(skillsDir, name);
|
|
226
|
+
try {
|
|
227
|
+
return statSync(full).isDirectory() || statSync(full).isSymbolicLink();
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
const skills = [];
|
|
234
|
+
for (const name of entries) {
|
|
235
|
+
const skillMd = resolve(skillsDir, name, "SKILL.md");
|
|
236
|
+
if (!existsSync(skillMd))
|
|
237
|
+
continue;
|
|
238
|
+
let content = "";
|
|
239
|
+
try {
|
|
240
|
+
content = readFileSync(skillMd, "utf8");
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const fm = parseSkillFrontmatter(content);
|
|
246
|
+
if (!fm.name)
|
|
247
|
+
continue;
|
|
248
|
+
skills.push({ name: fm.name, description: fm.description ?? "" });
|
|
249
|
+
}
|
|
250
|
+
if (skills.length === 0) {
|
|
251
|
+
return "No skills installed (expected at ~/.openclaw/skills).";
|
|
252
|
+
}
|
|
253
|
+
const lines = ["**Skills**"];
|
|
254
|
+
for (const s of skills) {
|
|
255
|
+
lines.push(`- \`!skill ${s.name}\`${s.description ? ` - ${s.description}` : ""}`);
|
|
256
|
+
}
|
|
257
|
+
lines.push("", "Run a skill: `!skill <name>`");
|
|
258
|
+
return lines.join("\n");
|
|
259
|
+
}
|
|
260
|
+
async function runGroups(ctx) {
|
|
261
|
+
try {
|
|
262
|
+
const subs = await ctx.client.listSubscriptions(null);
|
|
263
|
+
const rooms = subs
|
|
264
|
+
.filter((s) => s.t === "c" || s.t === "p")
|
|
265
|
+
.map((s) => (s.fname ? `#${s.fname}` : s.name ? `#${s.name}` : ""))
|
|
266
|
+
.filter(Boolean);
|
|
267
|
+
if (rooms.length === 0) {
|
|
268
|
+
return "This bot is not in any groups or channels yet.";
|
|
269
|
+
}
|
|
270
|
+
return ["**Groups this bot is in**", ...rooms.map((r) => `- ${r}`)].join("\n");
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return "Could not load group memberships.";
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function runAccess(ctx) {
|
|
277
|
+
const account = ctx.account;
|
|
278
|
+
const mention = account.mentionNames[0] ?? account.accountId;
|
|
279
|
+
const owner = readOwner(account.accountId);
|
|
280
|
+
const store = new AccessStore();
|
|
281
|
+
const grants = store.loadGrants(account.accountId);
|
|
282
|
+
store.close();
|
|
283
|
+
const lines = [];
|
|
284
|
+
lines.push(`owner - ${owner ? `${owner}` : "(unset)"}`);
|
|
285
|
+
if (grants.length === 0) {
|
|
286
|
+
lines.push("No grants. Only the owner can use the bot.");
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// Group grants per user so a user with both group + DM access shows on one line.
|
|
290
|
+
const byUser = new Map();
|
|
291
|
+
for (const grant of grants) {
|
|
292
|
+
const scope = grant.roomId === "*"
|
|
293
|
+
? "everywhere (group + dm)"
|
|
294
|
+
: grant.roomId === DM_SCOPE
|
|
295
|
+
? "dm"
|
|
296
|
+
: `group: #${grant.roomName ?? grant.roomId}`;
|
|
297
|
+
const list = byUser.get(grant.username) ?? [];
|
|
298
|
+
list.push(scope);
|
|
299
|
+
byUser.set(grant.username, list);
|
|
300
|
+
}
|
|
301
|
+
for (const [username, scopes] of byUser) {
|
|
302
|
+
lines.push(`- ${username} - ${scopes.join(", ")}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return [`**Access for ${mention}**`, ...lines].join("\n");
|
|
306
|
+
}
|
|
307
|
+
function adminAuth(ctx) {
|
|
308
|
+
const { userId, accessToken } = ctx.account.auth;
|
|
309
|
+
return { userId, authToken: accessToken };
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Resolve an admin credential to call privileged APIs (e.g. users.create).
|
|
313
|
+
* Prefers the dedicated admin credential saved during setup; falls back to the
|
|
314
|
+
* bot account's own token if no admin was configured.
|
|
315
|
+
*/
|
|
316
|
+
async function adminAuthForServer(serverUrl, ctx) {
|
|
317
|
+
const admin = await loadAdmin(serverUrl);
|
|
318
|
+
if (admin)
|
|
319
|
+
return { userId: admin.userId, authToken: admin.authToken };
|
|
320
|
+
return adminAuth(ctx);
|
|
321
|
+
}
|
|
322
|
+
/** Parse `key value --flag value` style args into a positional list + flags map. */
|
|
323
|
+
function parseArgs(argStr) {
|
|
324
|
+
const tokens = argStr.trim().length ? argStr.trim().split(/\s+/) : [];
|
|
325
|
+
const positional = [];
|
|
326
|
+
const flags = {};
|
|
327
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
328
|
+
const tok = tokens[i];
|
|
329
|
+
if (tok.startsWith("--")) {
|
|
330
|
+
const key = tok.slice(2);
|
|
331
|
+
const next = tokens[i + 1];
|
|
332
|
+
if (next && !next.startsWith("--")) {
|
|
333
|
+
flags[key] = next;
|
|
334
|
+
i++;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
flags[key] = "true";
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
positional.push(tok);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return { positional, flags };
|
|
345
|
+
}
|
|
346
|
+
async function runStatus(ctx) {
|
|
347
|
+
const mention = ctx.account.mentionNames[0] ?? ctx.account.accountId;
|
|
348
|
+
const gateway = connectionStatus.get(ctx.account.accountId) ??
|
|
349
|
+
(activeClients.has(ctx.account.accountId) ? "online" : "stopped");
|
|
350
|
+
const bindings = readBindingsForAccount(ctx.account.accountId);
|
|
351
|
+
const agent = bindings[0]?.agentId ?? "(unbound)";
|
|
352
|
+
const runtime = ctx.channelRuntime ? "ready" : "unavailable";
|
|
353
|
+
return [
|
|
354
|
+
"**Status**",
|
|
355
|
+
`- gateway - ${gateway}`,
|
|
356
|
+
`- bot - ${mention}`,
|
|
357
|
+
`- agent - ${agent}`,
|
|
358
|
+
`- runtime - ${runtime}`,
|
|
359
|
+
].join("\n");
|
|
360
|
+
}
|
|
361
|
+
async function runAddBot(ctx, argStr) {
|
|
362
|
+
const { positional, flags } = parseArgs(argStr);
|
|
363
|
+
const username = positional[0];
|
|
364
|
+
if (!username) {
|
|
365
|
+
return [
|
|
366
|
+
"**Create a new bot user**",
|
|
367
|
+
'Usage: `!add-bot <username> [--name "..."] [--email ...] [--agent <id>]`',
|
|
368
|
+
"",
|
|
369
|
+
"Examples:",
|
|
370
|
+
" `!add-bot alice` - quick create; auto-creates a dedicated agent `rc-alice`",
|
|
371
|
+
' `!add-bot alice --name "Alice Smith" --email alice@example.com --agent support`',
|
|
372
|
+
"",
|
|
373
|
+
"By default each bot gets its own dedicated agent (`rc-<username>`), so memory is isolated. " +
|
|
374
|
+
"Pass `--agent <id>` to bind the bot to an existing shared agent instead (e.g. `main`, `work`).",
|
|
375
|
+
"",
|
|
376
|
+
"A random password is generated and shown once. The bot comes online automatically.",
|
|
377
|
+
].join("\n");
|
|
378
|
+
}
|
|
379
|
+
const name = flags.name ?? username;
|
|
380
|
+
const email = flags.email ?? `${username.toLowerCase()}@openclaw.local`;
|
|
381
|
+
const password = randomToken(16);
|
|
382
|
+
let agent;
|
|
383
|
+
let agentNote = "";
|
|
384
|
+
if (flags.agent) {
|
|
385
|
+
agent = flags.agent;
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
const result = ensureAgentForBot(username);
|
|
389
|
+
agent = result.agentId;
|
|
390
|
+
if (result.fallback) {
|
|
391
|
+
return [
|
|
392
|
+
`Created bot ${username}, but could not auto-create a dedicated agent.`,
|
|
393
|
+
`Falling back to 'main' - memory is isolated per-bot via session keys, but the bot shares the main agent workspace.`,
|
|
394
|
+
].join("\n");
|
|
395
|
+
}
|
|
396
|
+
if (result.created) {
|
|
397
|
+
agentNote = ` (auto-created dedicated agent '${agent}')`;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
const limitCheck = checkBotCreationLimit("inline", { serverUrl: ctx.account.serverUrl });
|
|
402
|
+
if (!limitCheck.allowed) {
|
|
403
|
+
return limitCheck.reason ?? "Bot creation limit reached.";
|
|
404
|
+
}
|
|
405
|
+
const auth = await adminAuthForServer(ctx.account.serverUrl, ctx);
|
|
406
|
+
const existingServer = await getUserInfo(ctx.account.serverUrl, auth, { username });
|
|
407
|
+
const existingConfig = readAccount(username.toLowerCase());
|
|
408
|
+
if (existingServer) {
|
|
409
|
+
return `Bot ${username} already exists on the Rocket.Chat server. Use \`!remove-bot ${username}\` first if you want to recreate it.`;
|
|
410
|
+
}
|
|
411
|
+
if (existingConfig) {
|
|
412
|
+
return `Bot ${username} already exists in OpenClaw config. Use \`!remove-bot ${username}\` first if you want to recreate it.`;
|
|
413
|
+
}
|
|
414
|
+
await createBotUser(ctx.account.serverUrl, auth, { username, name, password, email });
|
|
415
|
+
const botAuth = await loginAs(ctx.account.serverUrl, username, password);
|
|
416
|
+
recordBotCreation(username, "inline");
|
|
417
|
+
const accountId = username;
|
|
418
|
+
addAccount({
|
|
419
|
+
accountId,
|
|
420
|
+
serverUrl: ctx.account.serverUrl,
|
|
421
|
+
auth: { mode: "token", userId: botAuth.userId, accessToken: botAuth.authToken },
|
|
422
|
+
mentionNames: [username],
|
|
423
|
+
...(ctx.account.owner ? { owner: ctx.account.owner } : {}),
|
|
424
|
+
});
|
|
425
|
+
addBinding({ channel: "rocketchat", accountId, agentId: agent });
|
|
426
|
+
const owner = ctx.account.owner?.trim().replace(/^@+/, "") || undefined;
|
|
427
|
+
let dmNote = "";
|
|
428
|
+
if (owner) {
|
|
429
|
+
try {
|
|
430
|
+
const dmRoom = await createDirectMessage(ctx.account.serverUrl, botAuth, owner);
|
|
431
|
+
await sendMessage(ctx.account.serverUrl, botAuth, dmRoom, `Hi! I'm ${username}, your new Rocket.Chat bot connected to OpenClaw (agent \`${agent}\`). ` +
|
|
432
|
+
`Once you see status online, confirm with \`!status\` or \`!help\` to know more.`);
|
|
433
|
+
dmNote = `Welcome DM sent to ${owner}.`;
|
|
434
|
+
}
|
|
435
|
+
catch (e) {
|
|
436
|
+
dmNote = `Could not DM ${owner}: ${e instanceof Error ? e.message : String(e)}`;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
void startBotAccount(ctx, {
|
|
440
|
+
accountId,
|
|
441
|
+
serverUrl: ctx.account.serverUrl,
|
|
442
|
+
auth: { mode: "token", userId: botAuth.userId, accessToken: botAuth.authToken },
|
|
443
|
+
...(owner ? { owner } : {}),
|
|
444
|
+
agent,
|
|
445
|
+
});
|
|
446
|
+
return [
|
|
447
|
+
`**Created bot ${username}**`,
|
|
448
|
+
`agent - ${agent}${agentNote}`,
|
|
449
|
+
`password (shown once) - \`${password}\``,
|
|
450
|
+
...(dmNote ? [dmNote] : []),
|
|
451
|
+
].join("\n");
|
|
452
|
+
}
|
|
453
|
+
catch (e) {
|
|
454
|
+
return `Failed to create bot: ${e instanceof Error ? e.message : String(e)}`;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/** Bring a freshly created bot account online immediately (hot-start), without a full OpenClaw restart. */
|
|
458
|
+
function startBotAccount(ctx, account) {
|
|
459
|
+
const controller = new AbortController();
|
|
460
|
+
const botCtx = {
|
|
461
|
+
accountId: account.accountId,
|
|
462
|
+
account: {
|
|
463
|
+
accountId: account.accountId,
|
|
464
|
+
enabled: true,
|
|
465
|
+
serverUrl: account.serverUrl,
|
|
466
|
+
auth: account.auth,
|
|
467
|
+
transport: { mode: "websocket" },
|
|
468
|
+
mentionNames: [account.accountId],
|
|
469
|
+
...(account.owner ? { owner: account.owner } : {}),
|
|
470
|
+
},
|
|
471
|
+
channelRuntime: ctx.channelRuntime,
|
|
472
|
+
abortSignal: controller.signal,
|
|
473
|
+
setStatus: (status) => console.log(`[RC] [rocketchat:${account.accountId}] hot-start: ${status}`),
|
|
474
|
+
};
|
|
475
|
+
startGateway(botCtx).catch((err) => console.error(`[RC] [rocketchat:${account.accountId}] hot-start failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Notify the affected user about a lend/revoke. The notice is always sent
|
|
479
|
+
* via DM to the user so that group chats only show the command reply, not a
|
|
480
|
+
* second personal notification. Returns an error note to append to the
|
|
481
|
+
* command reply if the notice could not be delivered.
|
|
482
|
+
*/
|
|
483
|
+
async function notifyAccessChange(ctx, cleanUser, roomId, scopeLabel, action, targetIsBot) {
|
|
484
|
+
const botMention = ctx.account.mentionNames[0] ?? ctx.accountId;
|
|
485
|
+
const how = roomId === DM_SCOPE ? "You can now DM the bot." : "You can now mention the bot there.";
|
|
486
|
+
const text = action === "granted"
|
|
487
|
+
? `You've been granted access to ${botMention} in ${scopeLabel}. ${how}`
|
|
488
|
+
: `Your access to ${botMention} in ${scopeLabel} has been revoked.`;
|
|
489
|
+
try {
|
|
490
|
+
const botAuth = adminAuth(ctx);
|
|
491
|
+
const dmRoomId = await createDirectMessage(ctx.account.serverUrl, botAuth, cleanUser);
|
|
492
|
+
await ctx.client.postMessage(dmRoomId, text);
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
catch (e) {
|
|
496
|
+
return ` (notice to ${cleanUser} failed: ${e instanceof Error ? e.message : String(e)})`;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function runLend(ctx, argStr) {
|
|
500
|
+
const { positional, flags } = parseArgs(argStr);
|
|
501
|
+
const wantDm = flags.dm === "true";
|
|
502
|
+
let groupName;
|
|
503
|
+
let username;
|
|
504
|
+
if (positional[0]?.toLowerCase() === "dm") {
|
|
505
|
+
username = positional[1];
|
|
506
|
+
groupName = undefined;
|
|
507
|
+
}
|
|
508
|
+
else if (wantDm) {
|
|
509
|
+
username = positional[0];
|
|
510
|
+
groupName = undefined;
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
groupName = positional[0];
|
|
514
|
+
username = positional[1];
|
|
515
|
+
}
|
|
516
|
+
if (!username)
|
|
517
|
+
return [
|
|
518
|
+
"Usage:",
|
|
519
|
+
" `!lend <group> <user>` - grant group/channel access",
|
|
520
|
+
" `!lend dm <user>` (or `!lend <user> --dm`) - grant DM access only",
|
|
521
|
+
].join("\n");
|
|
522
|
+
const cleanUser = username.replace(/^@+/, "");
|
|
523
|
+
try {
|
|
524
|
+
const auth = await adminAuthForServer(ctx.account.serverUrl, ctx);
|
|
525
|
+
const targetUser = await getUserInfo(ctx.account.serverUrl, auth, { username: cleanUser });
|
|
526
|
+
if (!targetUser)
|
|
527
|
+
return `User ${cleanUser} not found on the Rocket.Chat server.`;
|
|
528
|
+
let roomId;
|
|
529
|
+
let roomName;
|
|
530
|
+
if (groupName) {
|
|
531
|
+
const group = await getGroupByName(ctx.account.serverUrl, auth, groupName);
|
|
532
|
+
if (!group)
|
|
533
|
+
return `Group "${groupName}" not found.`;
|
|
534
|
+
roomId = group._id;
|
|
535
|
+
roomName = group.name;
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
roomId = DM_SCOPE;
|
|
539
|
+
roomName = "direct";
|
|
540
|
+
}
|
|
541
|
+
const denial = assertCanDelegate(ctx, roomId);
|
|
542
|
+
if (denial)
|
|
543
|
+
return denial;
|
|
544
|
+
const store = new AccessStore();
|
|
545
|
+
const ok = store.addGrant({
|
|
546
|
+
accountId: ctx.accountId,
|
|
547
|
+
roomId,
|
|
548
|
+
roomName,
|
|
549
|
+
username: cleanUser,
|
|
550
|
+
...(ctx.account.owner ? { grantedBy: ctx.account.owner } : {}),
|
|
551
|
+
});
|
|
552
|
+
store.close();
|
|
553
|
+
const scope = roomId === DM_SCOPE ? "direct messages" : `#${roomName}`;
|
|
554
|
+
if (ok) {
|
|
555
|
+
const notice = await notifyAccessChange(ctx, cleanUser, roomId, scope, "granted", !!targetUser.roles?.includes("bot"));
|
|
556
|
+
return `Granted ${cleanUser} access to ${ctx.account.mentionNames[0] ?? ctx.accountId} in ${scope}.${notice ?? ""}`;
|
|
557
|
+
}
|
|
558
|
+
return `That grant already exists.`;
|
|
559
|
+
}
|
|
560
|
+
catch (e) {
|
|
561
|
+
return `Failed to lend: ${e instanceof Error ? e.message : String(e)}`;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async function runRevoke(ctx, argStr) {
|
|
565
|
+
const { positional, flags } = parseArgs(argStr);
|
|
566
|
+
const wantDm = flags.dm === "true";
|
|
567
|
+
let groupName;
|
|
568
|
+
let username;
|
|
569
|
+
if (positional[0]?.toLowerCase() === "dm") {
|
|
570
|
+
username = positional[1];
|
|
571
|
+
groupName = undefined;
|
|
572
|
+
}
|
|
573
|
+
else if (wantDm) {
|
|
574
|
+
username = positional[0];
|
|
575
|
+
groupName = undefined;
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
groupName = positional[0];
|
|
579
|
+
username = positional[1];
|
|
580
|
+
}
|
|
581
|
+
if (!username)
|
|
582
|
+
return [
|
|
583
|
+
"Usage:",
|
|
584
|
+
" `!revoke <group> <user>` - remove a user's group/channel access",
|
|
585
|
+
" `!revoke dm <user>` (or `!revoke <user> --dm`) - remove a user's DM access",
|
|
586
|
+
].join("\n");
|
|
587
|
+
const cleanUser = username.replace(/^@+/, "");
|
|
588
|
+
try {
|
|
589
|
+
const auth = await adminAuthForServer(ctx.account.serverUrl, ctx);
|
|
590
|
+
const targetUser = await getUserInfo(ctx.account.serverUrl, auth, { username: cleanUser });
|
|
591
|
+
if (!targetUser)
|
|
592
|
+
return `User ${cleanUser} not found on the Rocket.Chat server.`;
|
|
593
|
+
let roomId;
|
|
594
|
+
let roomName;
|
|
595
|
+
if (groupName) {
|
|
596
|
+
const group = await getGroupByName(ctx.account.serverUrl, auth, groupName);
|
|
597
|
+
if (!group)
|
|
598
|
+
return `Group "${groupName}" not found.`;
|
|
599
|
+
roomId = group._id;
|
|
600
|
+
roomName = group.name;
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
roomId = DM_SCOPE;
|
|
604
|
+
roomName = "direct";
|
|
605
|
+
}
|
|
606
|
+
const store = new AccessStore();
|
|
607
|
+
const ok = store.removeGrant({
|
|
608
|
+
accountId: ctx.accountId,
|
|
609
|
+
roomId,
|
|
610
|
+
username: cleanUser,
|
|
611
|
+
...(ctx.account.owner ? { revokedBy: ctx.account.owner } : {}),
|
|
612
|
+
});
|
|
613
|
+
store.close();
|
|
614
|
+
const scope = roomId === DM_SCOPE ? "direct messages" : `#${roomName}`;
|
|
615
|
+
if (ok) {
|
|
616
|
+
const notice = await notifyAccessChange(ctx, cleanUser, roomId, scope, "revoked", !!targetUser.roles?.includes("bot"));
|
|
617
|
+
return `Revoked ${cleanUser}'s access to ${ctx.account.mentionNames[0] ?? ctx.accountId} in ${scope}.${notice ?? ""}`;
|
|
618
|
+
}
|
|
619
|
+
return `No such grant found. ${cleanUser} did not have access in ${scope}.`;
|
|
620
|
+
}
|
|
621
|
+
catch (e) {
|
|
622
|
+
return `Failed to revoke: ${e instanceof Error ? e.message : String(e)}`;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Non-owners may only delegate access within scopes where they themselves hold a grant.
|
|
627
|
+
* Returns an error string when delegation is not allowed, otherwise undefined.
|
|
628
|
+
*/
|
|
629
|
+
function assertCanDelegate(ctx, roomId) {
|
|
630
|
+
const owner = ctx.account.owner?.trim().replace(/^@+/, "").toLowerCase();
|
|
631
|
+
const actor = ctx.senderName?.trim().replace(/^@+/, "").toLowerCase();
|
|
632
|
+
if (!actor || actor === owner)
|
|
633
|
+
return undefined;
|
|
634
|
+
const store = new AccessStore();
|
|
635
|
+
const actorGrants = store.loadGrants(ctx.accountId);
|
|
636
|
+
store.close();
|
|
637
|
+
const allowed = actorGrants.some((g) => {
|
|
638
|
+
const gu = g.username.trim().replace(/^@+/, "").toLowerCase();
|
|
639
|
+
if (gu !== actor)
|
|
640
|
+
return false;
|
|
641
|
+
if (roomId === DM_SCOPE)
|
|
642
|
+
return g.roomId === DM_SCOPE;
|
|
643
|
+
return g.roomId === "*" || g.roomId === roomId;
|
|
644
|
+
});
|
|
645
|
+
return allowed ? undefined : "You can only grant access in spaces where you already have access.";
|
|
646
|
+
}
|
|
647
|
+
async function runRemoveBot(ctx, argStr) {
|
|
648
|
+
const { positional } = parseArgs(argStr);
|
|
649
|
+
const usernames = positional.map((p) => p.trim().replace(/^@+/, "")).filter(Boolean);
|
|
650
|
+
if (usernames.length === 0)
|
|
651
|
+
return "Usage: `!remove-bot <username...>` - delete one or more bot accounts (server user + OpenClaw config)";
|
|
652
|
+
try {
|
|
653
|
+
const auth = await adminAuthForServer(ctx.account.serverUrl, ctx);
|
|
654
|
+
const blocks = [];
|
|
655
|
+
let removedCount = 0;
|
|
656
|
+
for (const username of usernames) {
|
|
657
|
+
const result = await removeSingleBot(ctx, auth, username);
|
|
658
|
+
if (result.removed)
|
|
659
|
+
removedCount++;
|
|
660
|
+
blocks.push(result.text);
|
|
661
|
+
}
|
|
662
|
+
return [`**Removed ${removedCount}/${usernames.length} bot(s)**`, ...blocks].join("\n");
|
|
663
|
+
}
|
|
664
|
+
catch (e) {
|
|
665
|
+
return `Failed to remove bot(s): ${e instanceof Error ? e.message : String(e)}`;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async function removeSingleBot(ctx, auth, username) {
|
|
669
|
+
const configured = readAccount(username);
|
|
670
|
+
const onServer = await getUserInfo(ctx.account.serverUrl, auth, { username });
|
|
671
|
+
if (!configured && !onServer) {
|
|
672
|
+
return {
|
|
673
|
+
removed: false,
|
|
674
|
+
text: `- ${username}: not found on server or in OpenClaw config. Skipped.`,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
activeClients.delete(username);
|
|
678
|
+
let serverNote = "";
|
|
679
|
+
try {
|
|
680
|
+
await deleteUser(ctx.account.serverUrl, auth, username);
|
|
681
|
+
serverNote = "Rocket.Chat user deleted.";
|
|
682
|
+
}
|
|
683
|
+
catch (e) {
|
|
684
|
+
serverNote = `Could not delete Rocket.Chat user: ${e instanceof Error ? e.message : String(e)}`;
|
|
685
|
+
}
|
|
686
|
+
const existingBindings = readBindingsForAccount(username);
|
|
687
|
+
const boundAgent = existingBindings[0]?.agentId;
|
|
688
|
+
const ownsDedicatedAgent = boundAgent === `rc-${username}`;
|
|
689
|
+
removeBindingsForAccount(username);
|
|
690
|
+
removeAccount(username);
|
|
691
|
+
if (ownsDedicatedAgent) {
|
|
692
|
+
removeAgentDir(username);
|
|
693
|
+
}
|
|
694
|
+
const lines = [
|
|
695
|
+
`- ${username}: ${serverNote}`,
|
|
696
|
+
` OpenClaw config account + agent binding removed.`,
|
|
697
|
+
ownsDedicatedAgent
|
|
698
|
+
? ` Agent workspace \`rc-${username}\` removed.`
|
|
699
|
+
: ` Kept shared agent \`${boundAgent}\` (bot was not its owner).`,
|
|
700
|
+
];
|
|
701
|
+
return { removed: true, text: lines.join("\n") };
|
|
702
|
+
}
|
|
703
|
+
async function runAddGroup(ctx, argStr) {
|
|
704
|
+
const { positional } = parseArgs(argStr);
|
|
705
|
+
const groupName = positional[0]?.trim().replace(/^#+/, "");
|
|
706
|
+
const botName = positional[1]?.trim().replace(/^@+/, "") || ctx.account.mentionNames[0] || ctx.accountId;
|
|
707
|
+
if (!groupName)
|
|
708
|
+
return "Usage: `!add-group <group> [<bot>]` - invite a bot into a group/channel (defaults to this bot)";
|
|
709
|
+
try {
|
|
710
|
+
const auth = await adminAuthForServer(ctx.account.serverUrl, ctx);
|
|
711
|
+
const botExists = await getUserInfo(ctx.account.serverUrl, auth, { username: botName });
|
|
712
|
+
if (!botExists) {
|
|
713
|
+
return `Bot ${botName} not found on the Rocket.Chat server. Use \`!add-bot ${botName}\` to create it first.`;
|
|
714
|
+
}
|
|
715
|
+
const botConfigured = readAccount(botName.toLowerCase());
|
|
716
|
+
if (!botConfigured) {
|
|
717
|
+
return `${botName} exists on the server but is not configured in OpenClaw. Use \`!add-bot ${botName}\` to set it up.`;
|
|
718
|
+
}
|
|
719
|
+
const group = await getGroupByName(ctx.account.serverUrl, auth, groupName);
|
|
720
|
+
if (!group)
|
|
721
|
+
return `Group/channel "#${groupName}" not found.`;
|
|
722
|
+
const isPrivate = group.isPrivate;
|
|
723
|
+
const members = await listGroupMembers(ctx.account.serverUrl, auth, group._id);
|
|
724
|
+
const alreadyIn = members.some((m) => m.username.toLowerCase() === botName.toLowerCase());
|
|
725
|
+
if (alreadyIn) {
|
|
726
|
+
return `${botName} is already a member of #${group.name}. No action needed.`;
|
|
727
|
+
}
|
|
728
|
+
try {
|
|
729
|
+
await inviteToGroup(ctx.account.serverUrl, auth, group._id, botName, isPrivate);
|
|
730
|
+
}
|
|
731
|
+
catch (e) {
|
|
732
|
+
return `Failed to add ${botName} to #${group.name}: ${e instanceof Error ? e.message : String(e)}`;
|
|
733
|
+
}
|
|
734
|
+
return `Invited ${botName} to #${group.name}${isPrivate ? " (private group)" : " (channel)"}. The bot will start receiving messages there.`;
|
|
735
|
+
}
|
|
736
|
+
catch (e) {
|
|
737
|
+
return `Failed to add bot to group: ${e instanceof Error ? e.message : String(e)}`;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function randomToken(length) {
|
|
741
|
+
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
|
|
742
|
+
let out = "";
|
|
743
|
+
for (let i = 0; i < length; i++)
|
|
744
|
+
out += chars[Math.floor(Math.random() * chars.length)];
|
|
745
|
+
return out;
|
|
746
|
+
}
|
|
747
|
+
//# sourceMappingURL=channel.js.map
|