Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"url": "https://www.github.com/BlueBubblesApp/BlueBubbles-Server/issues"
},
"homepage": "https://www.bluebubbles.app",
"devEngines": {
"engines": {
"node": "20.11.x",
"npm": ">=10.x",
"python": ">=3.10.x"
Expand Down
Binary file not shown.
5 changes: 3 additions & 2 deletions packages/server/src/server/api/apple/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ const getServiceFromInput = (value: string) => {
if (valSplit.length <= 1) return "iMessage";

// Otherwise, return the "first" index in the array,
return valSplit[0];
const service = valSplit[0];
return service === "any" ? "iMessage" : service;
};

/**
Expand Down Expand Up @@ -183,7 +184,7 @@ export const sendMessage = (chatGuid: string, message: string, attachment: strin
// If the chat is to an individual, we need to make sure the number is formatted correctly
if (chatGuid.includes(";-;")) {
const strSplit = chatGuid.split(";-;");
const service = strSplit[0];
const service = strSplit[0] === "any" ? "iMessage" : strSplit[0];
const addr = strSplit[1];
chatGuid = `${service};-;${getiMessageAddressFormat(addr)}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ export class AttachmentRouter {

const qualities = ["good", "better", "best"];
if (!qualities.includes(quality as string)) {
throw new BadRequest({ error: `Invalid quality specified! Must be one of: ${qualities.join(', ')}` });
throw new BadRequest({
error: `Invalid quality specified! Must be one of: ${qualities.join(', ')}`
});
}

opts.quality = quality as "good" | "better" | "best";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import { Success } from "../responses/success";
import { Contact } from "@server/databases/server/entity";
import { parseWithQuery } from "../utils";
import { BadRequest } from "../responses/errors";
import { getLogger } from "@server/lib/logging/Loggable";

export class ContactRouter {
private static log = getLogger("ContactRouter");

private static isAddressObject(data: any): boolean {
return (
data &&
Expand Down Expand Up @@ -77,7 +80,7 @@ export class ContactRouter {
})
);
} catch (ex: any) {
console.log(ex);
ContactRouter.log.error(ex);
errors.push({
entry: item,
error: ex?.message ?? String(ex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ export class WebhookValidator {
// Find the webhook value in the webhook events
const webhookEvent = webhookEventOptions.find(e => e.value === event);
if (!webhookEvent) {
throw new BadRequest({ error: `Invalid webhook event: ${event}! Webhook must be one of: ${WebhookValidator.webhookValues}` });
throw new BadRequest({
error: `Invalid webhook event: ${event}! Webhook must be one of: ${WebhookValidator.webhookValues}`
});
}

// Update the event to the label
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export class PrivateApiFindMyEventHandler extends Loggable implements PrivateApi
for (const item of added) {
const handle = obfuscatedHandle(item?.handle);
if (item?.coordinates[0] === 0 && item?.coordinates[1] === 0) {
this.log.debug(`Received FindMy ${titleCase(item.status)} (0, 0) Location Update for Handle: ${handle}`);
this.log.debug(
`Received FindMy ${titleCase(item.status)} (0, 0) Location Update for Handle: ${handle}`
);
} else {
this.log.debug(`Received FindMy ${titleCase(item.status)} Location Update for Handle: ${handle}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export abstract class DylibPlugin extends Loggable {
try {
await FileSystem.executeAppleScript(hideApp(this.parentApp));
} catch (ex) {
console.log(ex);
this.log.error(ex instanceof Error ? ex.message : String(ex));
// Don't do anything
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class Message {
return this.dateRetracted ?? this.dateEdited ?? this.dateRead ?? this.dateDelivered ?? this.dateCreated;
}

get messageStatus(): String {
get messageStatus(): string {
return this.dateRetracted
? "Unsent"
: this.isFullyUnsent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const getConversionPath = (attachment: Attachment, extension: string): st
const newName = isEmpty(attachment.transferName) ? guid : attachment.transferName;

// If the path already has the extension, return it
let newPath = `${newDir}/${newName}`;
const newPath = `${newDir}/${newName}`;
if (newPath.endsWith(`.${extension}`)) return newPath;

// Otherwise, return the path with the extension
Expand Down
11 changes: 10 additions & 1 deletion packages/server/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1497,9 +1497,18 @@ class BlueBubblesServer extends EventEmitter {
}

private async handleNewMessage(item: Message) {
// If this is a dummy message with a UUID body (often emitted when an audio message is played), ignore it.
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/i;
if (item.text && isUUID.test(item.text) && !item.cacheHasAttachments) {
this.logger.info(`Ignoring audio note play receipt with UUID body: ${item.text}`);
return;
}

const newMessage = await insertChatParticipants(item);
this.logger.info(
`New Message from ${newMessage.isFromMe ? 'You' : obfuscatedHandle(newMessage.handle?.id)}, ${newMessage.contentString()}`);
`New Message from ${newMessage.isFromMe ? 'You' : obfuscatedHandle(newMessage.handle?.id)}, ` +
newMessage.contentString()
);

// Manually send the message to the socket so we can serialize it with
// all the extra data
Expand Down
100 changes: 56 additions & 44 deletions packages/server/src/server/managers/cloudflareManager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ export class CloudflareManager extends Loggable {
tag = "CloudflareManager";

daemonPath = path.join(
FileSystem.resources, "macos", "daemons", "cloudflare", (process.arch === "arm64") ? "arm64" : "x86", "cloudflared");
FileSystem.resources,
"macos",
"daemons",
"cloudflare",
(process.arch === "arm64") ? "arm64" : "x86",
"cloudflared"
);

// Use a default (empty) config file so we don't interfere with the default CF install (if any)
cfgPath = path.join(FileSystem.resources, "macos", "daemons", "cloudflare", "cloudflared-config.yml");
Expand All @@ -28,7 +34,10 @@ export class CloudflareManager extends Loggable {

async start(): Promise<string> {
if (this.isRateLimited) {
throw new Error("Cloudflare is rate limiting your requests. Waiting 1 hour... If you do not wawnt to wait 1 hour, fully restart the server.");
throw new Error(
"Cloudflare is rate limiting your requests. Waiting 1 hour... " +
"If you do not wawnt to wait 1 hour, fully restart the server."
);
}

try {
Expand All @@ -43,51 +52,48 @@ export class CloudflareManager extends Loggable {
}

private async connectHandler(): Promise<string> {
return new Promise(async (resolve, reject) => {
try {
const port = Server().repo.getConfig("socket_port") as string;
if (this.proc && !this.proc?.process?.killed) {
this.log.debug("Cloudflare Tunnel already running. Stopping...");
await this.stop();
}
const port = Server().repo.getConfig("socket_port") as string;
if (this.proc && !this.proc?.process?.killed) {
this.log.debug("Cloudflare Tunnel already running. Stopping...");
await this.stop();
}

this.log.debug("Starting Cloudflare Tunnel...");
this.proc = new ProcessSpawner({
command: this.daemonPath,
args: [
'tunnel',
'--url', `localhost:${port}`,
'--config', this.cfgPath,
'--pidfile', this.pidPath
],
verbose: true,
logTag: "CloudflareDaemon",
onOutput: (data) => this.handleData(data),
restartOnNonZeroExit: true,
restartOnNonZeroExitCondition: (_) => !this.isRateLimited,
waitForExit: false,
storeOutput: false
});

this.on("new-url", url => resolve(url));
this.on("error", err => {
// Ignore certain errors
if (typeof err === "string") {
if (err.includes("Thank you for trying Cloudflare Tunnel.")) return;
}
const connectPromise = new Promise<string>((resolve, reject) => {
this.on("new-url", url => resolve(url));
this.on("error", err => {
// Ignore certain errors
if (typeof err === "string") {
if (err.includes("Thank you for trying Cloudflare Tunnel.")) return;
}

reject(err);
});
reject(err);
});

setTimeout(() => {
reject(new Error("Failed to connect to Cloudflare after 2 minutes..."));
}, 1000 * 60 * 2); // 2 minutes
setTimeout(() => {
reject(new Error("Failed to connect to Cloudflare after 2 minutes..."));
}, 1000 * 60 * 2); // 2 minutes
});

await this.proc.execute();
} catch (ex) {
reject(ex);
}
this.log.debug("Starting Cloudflare Tunnel...");
this.proc = new ProcessSpawner({
command: this.daemonPath,
args: [
'tunnel',
'--url', `localhost:${port}`,
'--config', this.cfgPath,
'--pidfile', this.pidPath
],
verbose: true,
logTag: "CloudflareDaemon",
onOutput: (data) => this.handleData(data),
restartOnNonZeroExit: true,
restartOnNonZeroExitCondition: (_) => !this.isRateLimited,
waitForExit: false,
storeOutput: false
});

await this.proc.execute();
return connectPromise;
}

async stop() {
Expand Down Expand Up @@ -119,9 +125,15 @@ export class CloudflareManager extends Loggable {

private detectError(data: string): string | null {
if (data.includes('no such host')) {
return 'Unable to resolve api.trycloudflare.com! Ensure that your Mac has internet access and that any networking tools you use are not blocking the hostname.';
return (
'Unable to resolve api.trycloudflare.com! Ensure that your Mac has internet access and that any ' +
'networking tools you use are not blocking the hostname.'
);
} else if (data.includes("context deadline exceeded")) {
return "Failed to connect to Cloudflare's servers! Connection timed out. Please check your internet connection and try again.";
return (
"Failed to connect to Cloudflare's servers! Connection timed out. " +
"Please check your internet connection and try again."
);
} else if (data.includes("connect: bad file descriptor")) {
return "Failed to connect to Cloudflare's servers! Please make sure your Mac is up to date";
} else if (data.includes('failed to request quick Tunnel: ')) {
Expand Down
65 changes: 43 additions & 22 deletions packages/server/src/server/managers/zrokManager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ export class ZrokManager extends Loggable {
tag = "ZrokManager";

static get daemonPath() {
return path.join(FileSystem.resources, "macos", "daemons", "zrok", (process.arch === "arm64") ? "arm64" : "x86", "zrok");
return path.join(
FileSystem.resources,
"macos",
"daemons",
"zrok",
(process.arch === "arm64") ? "arm64" : "x86",
"zrok"
);
}

proc: ChildProcess;
Expand Down Expand Up @@ -48,32 +55,34 @@ export class ZrokManager extends Loggable {
const reservedTunnel = (Server().repo.getConfig("zrok_reserve_tunnel") as boolean) ?? false;
const tunnelToken = await ZrokManager.reserve(null);

return new Promise(async (resolve, reject) => {
// Didn't use zx here because I couldn't figure out how to pipe the stdout
// properly, without taking over the terminal outputs.
// Conditionally change the command based on if we are reserving a tunnel or not
const commndFlags = [
"share",
...(reservedTunnel ? ["reserved", "--headless"] : ["public", "--backend-mode", "proxy", "--headless"]),
...(reservedTunnel ? [tunnelToken] : [`0.0.0.0:${port}`])
];

if (this.proc && !this.proc?.killed) {
this.log.debug("Zrok Tunnel already running. Stopping...");
await this.stop();
}

this.proc = spawn(ZrokManager.daemonPath, commndFlags);
this.proc.stdout.on("data", chunk => this.handleData(chunk));
this.proc.stderr.on("data", chunk => this.handleData(chunk));
// Didn't use zx here because I couldn't figure out how to pipe the stdout
// properly, without taking over the terminal outputs.
// Conditionally change the command based on if we are reserving a tunnel or not
const commndFlags = [
"share",
...(reservedTunnel ? ["reserved", "--headless"] : ["public", "--backend-mode", "proxy", "--headless"]),
...(reservedTunnel ? [tunnelToken] : [`0.0.0.0:${port}`])
];

if (this.proc && !this.proc?.killed) {
this.log.debug("Zrok Tunnel already running. Stopping...");
await this.stop();
}

const connectPromise = new Promise<string>((resolve, reject) => {
this.on("new-url", url => resolve(url));
this.on("error", err => reject(err));

setTimeout(() => {
reject(new Error("Failed to connect to Zrok after 2 minutes..."));
}, 1000 * 60 * 2); // 2 minutes
});

this.proc = spawn(ZrokManager.daemonPath, commndFlags);
this.proc.stdout.on("data", chunk => this.handleData(chunk));
this.proc.stderr.on("data", chunk => this.handleData(chunk));

return connectPromise;
}

async stop() {
Expand Down Expand Up @@ -192,7 +201,10 @@ export class ZrokManager extends Loggable {

// If there is an existing token, release it
if (isNotEmpty(existingToken)) {
logger.info(`Releasing existing Zrok share (${existingToken}) because we no longer want to use a reserved tunnel.`);
logger.info(
`Releasing existing Zrok share (${existingToken}) because we no longer want to use ` +
`a reserved tunnel.`
);
await this.safeRelease(existingToken);
}

Expand All @@ -208,7 +220,11 @@ export class ZrokManager extends Loggable {
await ZrokManager.safeRelease(existingToken, { clearToken: true });
// If the tokens match, but the name doesn't match the token (which will be the name),
// then release the existing tunnel
} else if (existingToken === reservedToken && isNotEmpty(reservedName) && reservedName !== existingToken) {
} else if (
existingToken === reservedToken &&
isNotEmpty(reservedName) &&
reservedName !== existingToken
) {
logger.info(`Releasing existing Zrok share (${existingToken}) because the reserved name has changed.`);
await ZrokManager.safeRelease(existingToken, { clearToken: true });
// If we have an existing token and the name hasn't changed, return the existing token
Expand All @@ -228,7 +244,12 @@ export class ZrokManager extends Loggable {
}

logger.info(`Reserving new tunnel with flags: ${flags.join(" ")}`);
const output = await ProcessSpawner.executeCommand(this.daemonPath, ["reserve", "public", ...flags], {}, "ZrokManager");
const output = await ProcessSpawner.executeCommand(
this.daemonPath,
["reserve", "public", ...flags],
{},
"ZrokManager"
);
const urlMatches = output.match(ZrokManager.proxyUrlRegex);
if (isEmpty(urlMatches)) {
logger.info(`Failed to reserve Zrok tunnel! Unable to find URL in output. Output: ${output}`);
Expand Down
Loading