Skip to content

Commit 3e6dda2

Browse files
authored
Merge pull request #9 from openclaw/main
main
2 parents 8fd748c + 108f0ef commit 3e6dda2

64 files changed

Lines changed: 3684 additions & 3064 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

extensions/bluebubbles/src/attachments.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk";
22
import crypto from "node:crypto";
33
import path from "node:path";
44
import { resolveBlueBubblesAccount } from "./accounts.js";
5+
import { postMultipartFormData } from "./multipart.js";
56
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
67
import { extractBlueBubblesMessageId, resolveBlueBubblesSendTarget } from "./send-helpers.js";
78
import { resolveChatGuidForTarget } from "./send.js";
@@ -219,26 +220,12 @@ export async function sendBlueBubblesAttachment(params: {
219220
// Close the multipart body
220221
parts.push(encoder.encode(`--${boundary}--\r\n`));
221222

222-
// Combine all parts into a single buffer
223-
const totalLength = parts.reduce((acc, part) => acc + part.length, 0);
224-
const body = new Uint8Array(totalLength);
225-
let offset = 0;
226-
for (const part of parts) {
227-
body.set(part, offset);
228-
offset += part.length;
229-
}
230-
231-
const res = await blueBubblesFetchWithTimeout(
223+
const res = await postMultipartFormData({
232224
url,
233-
{
234-
method: "POST",
235-
headers: {
236-
"Content-Type": `multipart/form-data; boundary=${boundary}`,
237-
},
238-
body,
239-
},
240-
opts.timeoutMs ?? 60_000, // longer timeout for file uploads
241-
);
225+
boundary,
226+
parts,
227+
timeoutMs: opts.timeoutMs ?? 60_000, // longer timeout for file uploads
228+
});
242229

243230
if (!res.ok) {
244231
const errorText = await res.text();

extensions/bluebubbles/src/chat.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk";
22
import crypto from "node:crypto";
33
import path from "node:path";
44
import { resolveBlueBubblesAccount } from "./accounts.js";
5+
import { postMultipartFormData } from "./multipart.js";
56
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
67
import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js";
78

@@ -376,26 +377,12 @@ export async function setGroupIconBlueBubbles(
376377
// Close multipart body
377378
parts.push(encoder.encode(`--${boundary}--\r\n`));
378379

379-
// Combine into single buffer
380-
const totalLength = parts.reduce((acc, part) => acc + part.length, 0);
381-
const body = new Uint8Array(totalLength);
382-
let offset = 0;
383-
for (const part of parts) {
384-
body.set(part, offset);
385-
offset += part.length;
386-
}
387-
388-
const res = await blueBubblesFetchWithTimeout(
380+
const res = await postMultipartFormData({
389381
url,
390-
{
391-
method: "POST",
392-
headers: {
393-
"Content-Type": `multipart/form-data; boundary=${boundary}`,
394-
},
395-
body,
396-
},
397-
opts.timeoutMs ?? 60_000, // longer timeout for file uploads
398-
);
382+
boundary,
383+
parts,
384+
timeoutMs: opts.timeoutMs ?? 60_000, // longer timeout for file uploads
385+
});
399386

400387
if (!res.ok) {
401388
const errorText = await res.text().catch(() => "");
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { blueBubblesFetchWithTimeout } from "./types.js";
2+
3+
export function concatUint8Arrays(parts: Uint8Array[]): Uint8Array {
4+
const totalLength = parts.reduce((acc, part) => acc + part.length, 0);
5+
const body = new Uint8Array(totalLength);
6+
let offset = 0;
7+
for (const part of parts) {
8+
body.set(part, offset);
9+
offset += part.length;
10+
}
11+
return body;
12+
}
13+
14+
export async function postMultipartFormData(params: {
15+
url: string;
16+
boundary: string;
17+
parts: Uint8Array[];
18+
timeoutMs: number;
19+
}): Promise<Response> {
20+
const body = Buffer.from(concatUint8Arrays(params.parts));
21+
return await blueBubblesFetchWithTimeout(
22+
params.url,
23+
{
24+
method: "POST",
25+
headers: {
26+
"Content-Type": `multipart/form-data; boundary=${params.boundary}`,
27+
},
28+
body,
29+
},
30+
params.timeoutMs,
31+
);
32+
}

extensions/matrix/src/group-mentions.ts

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,33 @@ import type { CoreConfig } from "./types.js";
33
import { resolveMatrixAccountConfig } from "./matrix/accounts.js";
44
import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
55

6-
export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): boolean {
6+
function stripLeadingPrefixCaseInsensitive(value: string, prefix: string): string {
7+
return value.toLowerCase().startsWith(prefix.toLowerCase())
8+
? value.slice(prefix.length).trim()
9+
: value;
10+
}
11+
12+
function resolveMatrixRoomConfigForGroup(params: ChannelGroupContext) {
713
const rawGroupId = params.groupId?.trim() ?? "";
814
let roomId = rawGroupId;
9-
const lower = roomId.toLowerCase();
10-
if (lower.startsWith("matrix:")) {
11-
roomId = roomId.slice("matrix:".length).trim();
12-
}
13-
if (roomId.toLowerCase().startsWith("channel:")) {
14-
roomId = roomId.slice("channel:".length).trim();
15-
}
16-
if (roomId.toLowerCase().startsWith("room:")) {
17-
roomId = roomId.slice("room:".length).trim();
18-
}
15+
roomId = stripLeadingPrefixCaseInsensitive(roomId, "matrix:");
16+
roomId = stripLeadingPrefixCaseInsensitive(roomId, "channel:");
17+
roomId = stripLeadingPrefixCaseInsensitive(roomId, "room:");
18+
1919
const groupChannel = params.groupChannel?.trim() ?? "";
2020
const aliases = groupChannel ? [groupChannel] : [];
2121
const cfg = params.cfg as CoreConfig;
2222
const matrixConfig = resolveMatrixAccountConfig({ cfg, accountId: params.accountId });
23-
const resolved = resolveMatrixRoomConfig({
23+
return resolveMatrixRoomConfig({
2424
rooms: matrixConfig.groups ?? matrixConfig.rooms,
2525
roomId,
2626
aliases,
2727
name: groupChannel || undefined,
2828
}).config;
29+
}
30+
31+
export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): boolean {
32+
const resolved = resolveMatrixRoomConfigForGroup(params);
2933
if (resolved) {
3034
if (resolved.autoReply === true) {
3135
return false;
@@ -43,27 +47,6 @@ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): b
4347
export function resolveMatrixGroupToolPolicy(
4448
params: ChannelGroupContext,
4549
): GroupToolPolicyConfig | undefined {
46-
const rawGroupId = params.groupId?.trim() ?? "";
47-
let roomId = rawGroupId;
48-
const lower = roomId.toLowerCase();
49-
if (lower.startsWith("matrix:")) {
50-
roomId = roomId.slice("matrix:".length).trim();
51-
}
52-
if (roomId.toLowerCase().startsWith("channel:")) {
53-
roomId = roomId.slice("channel:".length).trim();
54-
}
55-
if (roomId.toLowerCase().startsWith("room:")) {
56-
roomId = roomId.slice("room:".length).trim();
57-
}
58-
const groupChannel = params.groupChannel?.trim() ?? "";
59-
const aliases = groupChannel ? [groupChannel] : [];
60-
const cfg = params.cfg as CoreConfig;
61-
const matrixConfig = resolveMatrixAccountConfig({ cfg, accountId: params.accountId });
62-
const resolved = resolveMatrixRoomConfig({
63-
rooms: matrixConfig.groups ?? matrixConfig.rooms,
64-
roomId,
65-
aliases,
66-
name: groupChannel || undefined,
67-
}).config;
50+
const resolved = resolveMatrixRoomConfigForGroup(params);
6851
return resolved?.tools;
6952
}

extensions/msteams/src/onboarding.ts

Lines changed: 29 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,32 @@ function looksLikeGuid(value: string): boolean {
6363
return /^[0-9a-fA-F-]{16,}$/.test(value);
6464
}
6565

66+
async function promptMSTeamsCredentials(prompter: WizardPrompter): Promise<{
67+
appId: string;
68+
appPassword: string;
69+
tenantId: string;
70+
}> {
71+
const appId = String(
72+
await prompter.text({
73+
message: "Enter MS Teams App ID",
74+
validate: (value) => (value?.trim() ? undefined : "Required"),
75+
}),
76+
).trim();
77+
const appPassword = String(
78+
await prompter.text({
79+
message: "Enter MS Teams App Password",
80+
validate: (value) => (value?.trim() ? undefined : "Required"),
81+
}),
82+
).trim();
83+
const tenantId = String(
84+
await prompter.text({
85+
message: "Enter MS Teams Tenant ID",
86+
validate: (value) => (value?.trim() ? undefined : "Required"),
87+
}),
88+
).trim();
89+
return { appId, appPassword, tenantId };
90+
}
91+
6692
async function promptMSTeamsAllowFrom(params: {
6793
cfg: OpenClawConfig;
6894
prompter: WizardPrompter;
@@ -251,69 +277,18 @@ export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = {
251277
},
252278
};
253279
} else {
254-
appId = String(
255-
await prompter.text({
256-
message: "Enter MS Teams App ID",
257-
validate: (value) => (value?.trim() ? undefined : "Required"),
258-
}),
259-
).trim();
260-
appPassword = String(
261-
await prompter.text({
262-
message: "Enter MS Teams App Password",
263-
validate: (value) => (value?.trim() ? undefined : "Required"),
264-
}),
265-
).trim();
266-
tenantId = String(
267-
await prompter.text({
268-
message: "Enter MS Teams Tenant ID",
269-
validate: (value) => (value?.trim() ? undefined : "Required"),
270-
}),
271-
).trim();
280+
({ appId, appPassword, tenantId } = await promptMSTeamsCredentials(prompter));
272281
}
273282
} else if (hasConfigCreds) {
274283
const keep = await prompter.confirm({
275284
message: "MS Teams credentials already configured. Keep them?",
276285
initialValue: true,
277286
});
278287
if (!keep) {
279-
appId = String(
280-
await prompter.text({
281-
message: "Enter MS Teams App ID",
282-
validate: (value) => (value?.trim() ? undefined : "Required"),
283-
}),
284-
).trim();
285-
appPassword = String(
286-
await prompter.text({
287-
message: "Enter MS Teams App Password",
288-
validate: (value) => (value?.trim() ? undefined : "Required"),
289-
}),
290-
).trim();
291-
tenantId = String(
292-
await prompter.text({
293-
message: "Enter MS Teams Tenant ID",
294-
validate: (value) => (value?.trim() ? undefined : "Required"),
295-
}),
296-
).trim();
288+
({ appId, appPassword, tenantId } = await promptMSTeamsCredentials(prompter));
297289
}
298290
} else {
299-
appId = String(
300-
await prompter.text({
301-
message: "Enter MS Teams App ID",
302-
validate: (value) => (value?.trim() ? undefined : "Required"),
303-
}),
304-
).trim();
305-
appPassword = String(
306-
await prompter.text({
307-
message: "Enter MS Teams App Password",
308-
validate: (value) => (value?.trim() ? undefined : "Required"),
309-
}),
310-
).trim();
311-
tenantId = String(
312-
await prompter.text({
313-
message: "Enter MS Teams Tenant ID",
314-
validate: (value) => (value?.trim() ? undefined : "Required"),
315-
}),
316-
).trim();
291+
({ appId, appPassword, tenantId } = await promptMSTeamsCredentials(prompter));
317292
}
318293

319294
if (appId && appPassword && tenantId) {

extensions/tlon/src/urbit/channel-client.ts

Lines changed: 25 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk";
2-
import { ensureUrbitChannelOpen } from "./channel-ops.js";
2+
import { ensureUrbitChannelOpen, pokeUrbitChannel, scryUrbitPath } from "./channel-ops.js";
33
import { getUrbitContext, normalizeUrbitCookie } from "./context.js";
44
import { urbitFetch } from "./fetch.js";
55

@@ -70,69 +70,35 @@ export class UrbitChannelClient {
7070

7171
async poke(params: { app: string; mark: string; json: unknown }): Promise<number> {
7272
await this.open();
73-
const pokeId = Date.now();
74-
const pokeData = {
75-
id: pokeId,
76-
action: "poke",
77-
ship: this.ship,
78-
app: params.app,
79-
mark: params.mark,
80-
json: params.json,
81-
};
82-
83-
const { response, release } = await urbitFetch({
84-
baseUrl: this.baseUrl,
85-
path: this.channelPath,
86-
init: {
87-
method: "PUT",
88-
headers: {
89-
"Content-Type": "application/json",
90-
Cookie: this.cookie,
91-
},
92-
body: JSON.stringify([pokeData]),
93-
},
94-
ssrfPolicy: this.ssrfPolicy,
95-
lookupFn: this.lookupFn,
96-
fetchImpl: this.fetchImpl,
97-
timeoutMs: 30_000,
98-
auditContext: "tlon-urbit-poke",
99-
});
100-
101-
try {
102-
if (!response.ok && response.status !== 204) {
103-
const errorText = await response.text().catch(() => "");
104-
throw new Error(`Poke failed: ${response.status}${errorText ? ` - ${errorText}` : ""}`);
105-
}
106-
return pokeId;
107-
} finally {
108-
await release();
73+
const channelId = this.channelId;
74+
if (!channelId) {
75+
throw new Error("Channel not opened");
10976
}
77+
return await pokeUrbitChannel(
78+
{
79+
baseUrl: this.baseUrl,
80+
cookie: this.cookie,
81+
ship: this.ship,
82+
channelId,
83+
ssrfPolicy: this.ssrfPolicy,
84+
lookupFn: this.lookupFn,
85+
fetchImpl: this.fetchImpl,
86+
},
87+
{ ...params, auditContext: "tlon-urbit-poke" },
88+
);
11089
}
11190

11291
async scry(path: string): Promise<unknown> {
113-
const scryPath = `/~/scry${path}`;
114-
const { response, release } = await urbitFetch({
115-
baseUrl: this.baseUrl,
116-
path: scryPath,
117-
init: {
118-
method: "GET",
119-
headers: { Cookie: this.cookie },
92+
return await scryUrbitPath(
93+
{
94+
baseUrl: this.baseUrl,
95+
cookie: this.cookie,
96+
ssrfPolicy: this.ssrfPolicy,
97+
lookupFn: this.lookupFn,
98+
fetchImpl: this.fetchImpl,
12099
},
121-
ssrfPolicy: this.ssrfPolicy,
122-
lookupFn: this.lookupFn,
123-
fetchImpl: this.fetchImpl,
124-
timeoutMs: 30_000,
125-
auditContext: "tlon-urbit-scry",
126-
});
127-
128-
try {
129-
if (!response.ok) {
130-
throw new Error(`Scry failed: ${response.status} for path ${path}`);
131-
}
132-
return await response.json();
133-
} finally {
134-
await release();
135-
}
100+
{ path, auditContext: "tlon-urbit-scry" },
101+
);
136102
}
137103

138104
async getOurName(): Promise<string> {

0 commit comments

Comments
 (0)