(Step 2) Phase 2 & 3 Complete + Reviewed

This commit is contained in:
Tyler Yust
2026-01-19 18:23:00 -08:00
committed by Peter Steinberger
parent ac2fcfe96a
commit e9d691d472
12 changed files with 2591 additions and 49 deletions

View File

@@ -64,3 +64,219 @@ export async function sendBlueBubblesTyping(
throw new Error(`BlueBubbles typing failed (${res.status}): ${errorText || "unknown"}`);
}
}
/**
* Edit a message via BlueBubbles API.
* Requires macOS 13 (Ventura) or higher with Private API enabled.
*/
export async function editBlueBubblesMessage(
messageGuid: string,
newText: string,
opts: BlueBubblesChatOpts & { partIndex?: number; backwardsCompatMessage?: string } = {},
): Promise<void> {
const trimmedGuid = messageGuid.trim();
if (!trimmedGuid) throw new Error("BlueBubbles edit requires messageGuid");
const trimmedText = newText.trim();
if (!trimmedText) throw new Error("BlueBubbles edit requires newText");
const { baseUrl, password } = resolveAccount(opts);
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/message/${encodeURIComponent(trimmedGuid)}/edit`,
password,
});
const payload = {
editedMessage: trimmedText,
backwardsCompatibilityMessage: opts.backwardsCompatMessage ?? `Edited to: ${trimmedText}`,
partIndex: typeof opts.partIndex === "number" ? opts.partIndex : 0,
};
const res = await blueBubblesFetchWithTimeout(
url,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
},
opts.timeoutMs,
);
if (!res.ok) {
const errorText = await res.text().catch(() => "");
throw new Error(`BlueBubbles edit failed (${res.status}): ${errorText || "unknown"}`);
}
}
/**
* Unsend (retract) a message via BlueBubbles API.
* Requires macOS 13 (Ventura) or higher with Private API enabled.
*/
export async function unsendBlueBubblesMessage(
messageGuid: string,
opts: BlueBubblesChatOpts & { partIndex?: number } = {},
): Promise<void> {
const trimmedGuid = messageGuid.trim();
if (!trimmedGuid) throw new Error("BlueBubbles unsend requires messageGuid");
const { baseUrl, password } = resolveAccount(opts);
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/message/${encodeURIComponent(trimmedGuid)}/unsend`,
password,
});
const payload = {
partIndex: typeof opts.partIndex === "number" ? opts.partIndex : 0,
};
const res = await blueBubblesFetchWithTimeout(
url,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
},
opts.timeoutMs,
);
if (!res.ok) {
const errorText = await res.text().catch(() => "");
throw new Error(`BlueBubbles unsend failed (${res.status}): ${errorText || "unknown"}`);
}
}
/**
* Rename a group chat via BlueBubbles API.
*/
export async function renameBlueBubblesChat(
chatGuid: string,
displayName: string,
opts: BlueBubblesChatOpts = {},
): Promise<void> {
const trimmedGuid = chatGuid.trim();
if (!trimmedGuid) throw new Error("BlueBubbles rename requires chatGuid");
const { baseUrl, password } = resolveAccount(opts);
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}`,
password,
});
const res = await blueBubblesFetchWithTimeout(
url,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ displayName }),
},
opts.timeoutMs,
);
if (!res.ok) {
const errorText = await res.text().catch(() => "");
throw new Error(`BlueBubbles rename failed (${res.status}): ${errorText || "unknown"}`);
}
}
/**
* Add a participant to a group chat via BlueBubbles API.
*/
export async function addBlueBubblesParticipant(
chatGuid: string,
address: string,
opts: BlueBubblesChatOpts = {},
): Promise<void> {
const trimmedGuid = chatGuid.trim();
if (!trimmedGuid) throw new Error("BlueBubbles addParticipant requires chatGuid");
const trimmedAddress = address.trim();
if (!trimmedAddress) throw new Error("BlueBubbles addParticipant requires address");
const { baseUrl, password } = resolveAccount(opts);
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}/participant`,
password,
});
const res = await blueBubblesFetchWithTimeout(
url,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address: trimmedAddress }),
},
opts.timeoutMs,
);
if (!res.ok) {
const errorText = await res.text().catch(() => "");
throw new Error(`BlueBubbles addParticipant failed (${res.status}): ${errorText || "unknown"}`);
}
}
/**
* Remove a participant from a group chat via BlueBubbles API.
*/
export async function removeBlueBubblesParticipant(
chatGuid: string,
address: string,
opts: BlueBubblesChatOpts = {},
): Promise<void> {
const trimmedGuid = chatGuid.trim();
if (!trimmedGuid) throw new Error("BlueBubbles removeParticipant requires chatGuid");
const trimmedAddress = address.trim();
if (!trimmedAddress) throw new Error("BlueBubbles removeParticipant requires address");
const { baseUrl, password } = resolveAccount(opts);
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}/participant`,
password,
});
const res = await blueBubblesFetchWithTimeout(
url,
{
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address: trimmedAddress }),
},
opts.timeoutMs,
);
if (!res.ok) {
const errorText = await res.text().catch(() => "");
throw new Error(`BlueBubbles removeParticipant failed (${res.status}): ${errorText || "unknown"}`);
}
}
/**
* Leave a group chat via BlueBubbles API.
*/
export async function leaveBlueBubblesChat(
chatGuid: string,
opts: BlueBubblesChatOpts = {},
): Promise<void> {
const trimmedGuid = chatGuid.trim();
if (!trimmedGuid) throw new Error("BlueBubbles leaveChat requires chatGuid");
const { baseUrl, password } = resolveAccount(opts);
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}/leave`,
password,
});
const res = await blueBubblesFetchWithTimeout(
url,
{ method: "POST" },
opts.timeoutMs,
);
if (!res.ok) {
const errorText = await res.text().catch(() => "");
throw new Error(`BlueBubbles leaveChat failed (${res.status}): ${errorText || "unknown"}`);
}
}